diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 3777dec1..00000000 --- a/backend/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -CPM_ENV=development -CPM_HTTP_PORT=8080 -CPM_DB_PATH=./data/cpm.db -CPM_CADDY_ADMIN_API=http://localhost:2019 -CPM_CADDY_CONFIG_DIR=./data/caddy diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 59e0b05f..00000000 --- a/backend/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Backend Service - -This folder contains the Go API for CaddyProxyManager+. - -## Prerequisites -- Go 1.22+ - -## Getting started -```bash -cp .env.example .env # optional -cd backend -go run ./cmd/api -``` - -## Tests -```bash -cd backend -go test ./... -``` diff --git a/backend/cmd/api/data/cpm.db b/backend/cmd/api/data/cpm.db deleted file mode 100644 index fff66f0a..00000000 Binary files a/backend/cmd/api/data/cpm.db and /dev/null differ diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go deleted file mode 100644 index 9417d9a2..00000000 --- a/backend/cmd/api/main.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/routes" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/database" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/server" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version" -) - -func main() { - log.Printf("starting %s backend on version %s", version.Name, version.Full()) - - cfg, err := config.Load() - if err != nil { - log.Fatalf("load config: %v", err) - } - - db, err := database.Connect(cfg.DatabasePath) - if err != nil { - log.Fatalf("connect database: %v", err) - } - - router := server.NewRouter(cfg.FrontendDir) - - // Pass config to routes for auth service and certificate service - if err := routes.Register(router, db, cfg); err != nil { - log.Fatalf("register routes: %v", err) - } - - // Register import handler with config dependencies - routes.RegisterImportHandler(router, db, cfg.CaddyBinary, cfg.ImportDir) - - // Check for mounted Caddyfile on startup - if err := handlers.CheckMountedImport(db, cfg.ImportCaddyfile, cfg.CaddyBinary, cfg.ImportDir); err != nil { - log.Printf("WARNING: failed to process mounted Caddyfile: %v", err) - } - - addr := fmt.Sprintf(":%s", cfg.HTTPPort) - log.Printf("starting %s backend on %s", version.Name, addr) - - if err := router.Run(addr); err != nil { - log.Fatalf("server error: %v", err) - } -} diff --git a/backend/cmd/seed/main.go b/backend/cmd/seed/main.go deleted file mode 100644 index 5b12b34c..00000000 --- a/backend/cmd/seed/main.go +++ /dev/null @@ -1,204 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "github.com/google/uuid" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -func main() { - // Connect to database - db, err := gorm.Open(sqlite.Open("./data/cpm.db"), &gorm.Config{}) - if err != nil { - log.Fatal("Failed to connect to database:", err) - } - - // Auto migrate - if err := db.AutoMigrate( - &models.User{}, - &models.ProxyHost{}, - &models.CaddyConfig{}, - &models.RemoteServer{}, - &models.SSLCertificate{}, - &models.AccessList{}, - &models.Setting{}, - &models.ImportSession{}, - ); err != nil { - log.Fatal("Failed to migrate database:", err) - } - - fmt.Println("✓ Database migrated successfully") - - // Seed Remote Servers - remoteServers := []models.RemoteServer{ - { - UUID: uuid.NewString(), - Name: "Local Docker Registry", - Provider: "docker", - Host: "localhost", - Port: 5000, - Scheme: "http", - Description: "Local Docker container registry", - Enabled: true, - Reachable: false, - }, - { - UUID: uuid.NewString(), - Name: "Development API Server", - Provider: "generic", - Host: "192.168.1.100", - Port: 8080, - Scheme: "http", - Description: "Main development API backend", - Enabled: true, - Reachable: false, - }, - { - UUID: uuid.NewString(), - Name: "Staging Web App", - Provider: "vm", - Host: "staging.internal", - Port: 3000, - Scheme: "http", - Description: "Staging environment web application", - Enabled: true, - Reachable: false, - }, - { - UUID: uuid.NewString(), - Name: "Database Admin", - Provider: "docker", - Host: "localhost", - Port: 8081, - Scheme: "http", - Description: "PhpMyAdmin or similar DB management tool", - Enabled: false, - Reachable: false, - }, - } - - for _, server := range remoteServers { - result := db.Where("host = ? AND port = ?", server.Host, server.Port).FirstOrCreate(&server) - if result.Error != nil { - log.Printf("Failed to seed remote server %s: %v", server.Name, result.Error) - } else if result.RowsAffected > 0 { - fmt.Printf("✓ Created remote server: %s (%s:%d)\n", server.Name, server.Host, server.Port) - } else { - fmt.Printf(" Remote server already exists: %s\n", server.Name) - } - } - - // Seed Proxy Hosts - proxyHosts := []models.ProxyHost{ - { - UUID: uuid.NewString(), - Name: "Development App", - DomainNames: "app.local.dev", - ForwardScheme: "http", - ForwardHost: "localhost", - ForwardPort: 3000, - SSLForced: false, - WebsocketSupport: true, - HSTSEnabled: false, - BlockExploits: true, - Enabled: true, - }, - { - UUID: uuid.NewString(), - Name: "API Server", - DomainNames: "api.local.dev", - ForwardScheme: "http", - ForwardHost: "192.168.1.100", - ForwardPort: 8080, - SSLForced: false, - WebsocketSupport: false, - HSTSEnabled: false, - BlockExploits: true, - Enabled: true, - }, - { - UUID: uuid.NewString(), - Name: "Docker Registry", - DomainNames: "docker.local.dev", - ForwardScheme: "http", - ForwardHost: "localhost", - ForwardPort: 5000, - SSLForced: false, - WebsocketSupport: false, - HSTSEnabled: false, - BlockExploits: true, - Enabled: false, - }, - } - - for _, host := range proxyHosts { - result := db.Where("domain_names = ?", host.DomainNames).FirstOrCreate(&host) - if result.Error != nil { - log.Printf("Failed to seed proxy host %s: %v", host.DomainNames, result.Error) - } else if result.RowsAffected > 0 { - fmt.Printf("✓ Created proxy host: %s -> %s://%s:%d\n", - host.DomainNames, host.ForwardScheme, host.ForwardHost, host.ForwardPort) - } else { - fmt.Printf(" Proxy host already exists: %s\n", host.DomainNames) - } - } - - // Seed Settings - settings := []models.Setting{ - { - Key: "app_name", - Value: "Caddy Proxy Manager+", - Type: "string", - Category: "general", - }, - { - Key: "default_scheme", - Value: "http", - Type: "string", - Category: "general", - }, - { - Key: "enable_ssl_by_default", - Value: "false", - Type: "bool", - Category: "security", - }, - } - - for _, setting := range settings { - result := db.Where("key = ?", setting.Key).FirstOrCreate(&setting) - if result.Error != nil { - log.Printf("Failed to seed setting %s: %v", setting.Key, result.Error) - } else if result.RowsAffected > 0 { - fmt.Printf("✓ Created setting: %s = %s\n", setting.Key, setting.Value) - } else { - fmt.Printf(" Setting already exists: %s\n", setting.Key) - } - } - - // Seed default admin user (for future authentication) - user := models.User{ - UUID: uuid.NewString(), - Email: "admin@localhost", - Name: "Administrator", - PasswordHash: "$2a$10$example_hashed_password", // This would be properly hashed in production - Role: "admin", - Enabled: true, - } - result := db.Where("email = ?", user.Email).FirstOrCreate(&user) - if result.Error != nil { - log.Printf("Failed to seed user: %v", result.Error) - } else if result.RowsAffected > 0 { - fmt.Printf("✓ Created default user: %s\n", user.Email) - } else { - fmt.Printf(" User already exists: %s\n", user.Email) - } - - fmt.Println("\n✓ Database seeding completed successfully!") - fmt.Println(" You can now start the application and see sample data.") -} diff --git a/backend/data/cpm.db b/backend/data/cpm.db deleted file mode 100644 index eb92eaf5..00000000 Binary files a/backend/data/cpm.db and /dev/null differ diff --git a/backend/go.mod b/backend/go.mod deleted file mode 100644 index 11a856bc..00000000 --- a/backend/go.mod +++ /dev/null @@ -1,46 +0,0 @@ -module github.com/Wikid82/CaddyProxyManagerPlus/backend - -go 1.25.4 - -require ( - github.com/gin-gonic/gin v1.10.0 - github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/google/uuid v1.6.0 - github.com/stretchr/testify v1.11.1 - golang.org/x/crypto v0.45.0 - gorm.io/driver/sqlite v1.6.0 - gorm.io/gorm v1.31.1 -) - -require ( - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/backend/go.sum b/backend/go.sum deleted file mode 100644 index 5b797192..00000000 --- a/backend/go.sum +++ /dev/null @@ -1,118 +0,0 @@ -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= -gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= -gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= -gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= -gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= -gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= -gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= -gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/internal/api/handlers/auth_handler.go b/backend/internal/api/handlers/auth_handler.go deleted file mode 100644 index 5bb42718..00000000 --- a/backend/internal/api/handlers/auth_handler.go +++ /dev/null @@ -1,99 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" - "github.com/gin-gonic/gin" -) - -type AuthHandler struct { - authService *services.AuthService -} - -func NewAuthHandler(authService *services.AuthService) *AuthHandler { - return &AuthHandler{authService: authService} -} - -type LoginRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required"` -} - -func (h *AuthHandler) Login(c *gin.Context) { - var req LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - token, err := h.authService.Login(req.Email, req.Password) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) - return - } - - // Set cookie - c.SetCookie("auth_token", token, 3600*24, "/", "", false, true) // Secure should be true in prod - - c.JSON(http.StatusOK, gin.H{"token": token}) -} - -type RegisterRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=8"` - Name string `json:"name" binding:"required"` -} - -func (h *AuthHandler) Register(c *gin.Context) { - var req RegisterRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - user, err := h.authService.Register(req.Email, req.Password, req.Name) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, user) -} - -func (h *AuthHandler) Logout(c *gin.Context) { - c.SetCookie("auth_token", "", -1, "/", "", false, true) - c.JSON(http.StatusOK, gin.H{"message": "Logged out"}) -} - -func (h *AuthHandler) Me(c *gin.Context) { - userID, _ := c.Get("userID") - role, _ := c.Get("role") - c.JSON(http.StatusOK, gin.H{"user_id": userID, "role": role}) -} - -type ChangePasswordRequest struct { - OldPassword string `json:"old_password" binding:"required"` - NewPassword string `json:"new_password" binding:"required,min=8"` -} - -func (h *AuthHandler) ChangePassword(c *gin.Context) { - var req ChangePasswordRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - userID, exists := c.Get("userID") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - if err := h.authService.ChangePassword(userID.(uint), req.OldPassword, req.NewPassword); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) -} diff --git a/backend/internal/api/handlers/certificate_handler.go b/backend/internal/api/handlers/certificate_handler.go deleted file mode 100644 index d46ad2d4..00000000 --- a/backend/internal/api/handlers/certificate_handler.go +++ /dev/null @@ -1,27 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" -) - -type CertificateHandler struct { - service *services.CertificateService -} - -func NewCertificateHandler(service *services.CertificateService) *CertificateHandler { - return &CertificateHandler{service: service} -} - -func (h *CertificateHandler) List(c *gin.Context) { - certs, err := h.service.ListCertificates() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, certs) -} diff --git a/backend/internal/api/handlers/handlers_test.go b/backend/internal/api/handlers/handlers_test.go deleted file mode 100644 index 5b7db0c5..00000000 --- a/backend/internal/api/handlers/handlers_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package handlers_test - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -func setupTestDB() *gorm.DB { - db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) - if err != nil { - panic("failed to connect to test database") - } - - // Auto migrate - db.AutoMigrate( - &models.ProxyHost{}, - &models.Location{}, - &models.RemoteServer{}, - &models.ImportSession{}, - ) - - return db -} - -func TestRemoteServerHandler_List(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - // Create test server - server := &models.RemoteServer{ - UUID: uuid.NewString(), - Name: "Test Server", - Provider: "docker", - Host: "localhost", - Port: 8080, - Enabled: true, - } - db.Create(server) - - handler := handlers.NewRemoteServerHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test List - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/remote-servers", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var servers []models.RemoteServer - err := json.Unmarshal(w.Body.Bytes(), &servers) - assert.NoError(t, err) - assert.Len(t, servers, 1) - assert.Equal(t, "Test Server", servers[0].Name) -} - -func TestRemoteServerHandler_Create(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - handler := handlers.NewRemoteServerHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test Create - serverData := map[string]interface{}{ - "name": "New Server", - "provider": "generic", - "host": "192.168.1.100", - "port": 3000, - "enabled": true, - } - body, _ := json.Marshal(serverData) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/remote-servers", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusCreated, w.Code) - - var server models.RemoteServer - err := json.Unmarshal(w.Body.Bytes(), &server) - assert.NoError(t, err) - assert.Equal(t, "New Server", server.Name) - assert.NotEmpty(t, server.UUID) -} - -func TestRemoteServerHandler_TestConnection(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - // Create test server - server := &models.RemoteServer{ - UUID: uuid.NewString(), - Name: "Test Server", - Provider: "docker", - Host: "localhost", - Port: 99999, // Invalid port to test failure - Enabled: true, - } - db.Create(server) - - handler := handlers.NewRemoteServerHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test connection - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/remote-servers/"+server.UUID+"/test", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var result map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &result) - assert.NoError(t, err) - assert.False(t, result["reachable"].(bool)) - assert.NotEmpty(t, result["error"]) -} - -func TestRemoteServerHandler_Get(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - // Create test server - server := &models.RemoteServer{ - UUID: uuid.NewString(), - Name: "Test Server", - Provider: "docker", - Host: "localhost", - Port: 8080, - Enabled: true, - } - db.Create(server) - - handler := handlers.NewRemoteServerHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test Get - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/remote-servers/"+server.UUID, nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var fetched models.RemoteServer - err := json.Unmarshal(w.Body.Bytes(), &fetched) - assert.NoError(t, err) - assert.Equal(t, server.UUID, fetched.UUID) -} - -func TestRemoteServerHandler_Update(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - // Create test server - server := &models.RemoteServer{ - UUID: uuid.NewString(), - Name: "Test Server", - Provider: "docker", - Host: "localhost", - Port: 8080, - Enabled: true, - } - db.Create(server) - - handler := handlers.NewRemoteServerHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test Update - updateData := map[string]interface{}{ - "name": "Updated Server", - "provider": "generic", - "host": "10.0.0.1", - "port": 9000, - "enabled": false, - } - body, _ := json.Marshal(updateData) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/v1/remote-servers/"+server.UUID, bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var updated models.RemoteServer - err := json.Unmarshal(w.Body.Bytes(), &updated) - assert.NoError(t, err) - assert.Equal(t, "Updated Server", updated.Name) - assert.Equal(t, "generic", updated.Provider) - assert.False(t, updated.Enabled) -} - -func TestRemoteServerHandler_Delete(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - // Create test server - server := &models.RemoteServer{ - UUID: uuid.NewString(), - Name: "Test Server", - Provider: "docker", - Host: "localhost", - Port: 8080, - Enabled: true, - } - db.Create(server) - - handler := handlers.NewRemoteServerHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test Delete - w := httptest.NewRecorder() - req, _ := http.NewRequest("DELETE", "/api/v1/remote-servers/"+server.UUID, nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusNoContent, w.Code) - - // Verify Delete - w2 := httptest.NewRecorder() - req2, _ := http.NewRequest("GET", "/api/v1/remote-servers/"+server.UUID, nil) - router.ServeHTTP(w2, req2) - - assert.Equal(t, http.StatusNotFound, w2.Code) -} - -func TestProxyHostHandler_List(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - // Create test proxy host - host := &models.ProxyHost{ - UUID: uuid.NewString(), - Name: "Test Host", - DomainNames: "test.local", - ForwardScheme: "http", - ForwardHost: "localhost", - ForwardPort: 3000, - Enabled: true, - } - db.Create(host) - - handler := handlers.NewProxyHostHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test List - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/proxy-hosts", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var hosts []models.ProxyHost - err := json.Unmarshal(w.Body.Bytes(), &hosts) - assert.NoError(t, err) - assert.Len(t, hosts, 1) - assert.Equal(t, "Test Host", hosts[0].Name) -} - -func TestProxyHostHandler_Create(t *testing.T) { - gin.SetMode(gin.TestMode) - db := setupTestDB() - - handler := handlers.NewProxyHostHandler(db) - router := gin.New() - handler.RegisterRoutes(router.Group("/api/v1")) - - // Test Create - hostData := map[string]interface{}{ - "name": "New Host", - "domain_names": "new.local", - "forward_scheme": "http", - "forward_host": "192.168.1.200", - "forward_port": 8080, - "enabled": true, - } - body, _ := json.Marshal(hostData) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/proxy-hosts", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusCreated, w.Code) - - var host models.ProxyHost - err := json.Unmarshal(w.Body.Bytes(), &host) - assert.NoError(t, err) - assert.Equal(t, "New Host", host.Name) - assert.Equal(t, "new.local", host.DomainNames) - assert.NotEmpty(t, host.UUID) -} - -func TestHealthHandler(t *testing.T) { - gin.SetMode(gin.TestMode) - - router := gin.New() - router.GET("/health", handlers.HealthHandler) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/health", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var result map[string]string - err := json.Unmarshal(w.Body.Bytes(), &result) - assert.NoError(t, err) - assert.Equal(t, "ok", result["status"]) -} diff --git a/backend/internal/api/handlers/health_handler.go b/backend/internal/api/handlers/health_handler.go deleted file mode 100644 index 2da47944..00000000 --- a/backend/internal/api/handlers/health_handler.go +++ /dev/null @@ -1,19 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version" - "github.com/gin-gonic/gin" -) - -// HealthHandler responds with basic service metadata for uptime checks. -func HealthHandler(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "service": version.Name, - "version": version.Version, - "git_commit": version.GitCommit, - "build_time": version.BuildTime, - }) -} diff --git a/backend/internal/api/handlers/import_handler.go b/backend/internal/api/handlers/import_handler.go deleted file mode 100644 index b0a0da35..00000000 --- a/backend/internal/api/handlers/import_handler.go +++ /dev/null @@ -1,285 +0,0 @@ -package handlers - -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/caddy" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" -) - -// ImportHandler handles Caddyfile import operations. -type ImportHandler struct { - db *gorm.DB - proxyHostSvc *services.ProxyHostService - importerservice *caddy.Importer - importDir string -} - -// NewImportHandler creates a new import handler. -func NewImportHandler(db *gorm.DB, caddyBinary, importDir string) *ImportHandler { - return &ImportHandler{ - db: db, - proxyHostSvc: services.NewProxyHostService(db), - importerservice: caddy.NewImporter(caddyBinary), - importDir: importDir, - } -} - -// RegisterRoutes registers import-related routes. -func (h *ImportHandler) RegisterRoutes(router *gin.RouterGroup) { - router.GET("/import/status", h.GetStatus) - router.GET("/import/preview", h.GetPreview) - router.POST("/import/upload", h.Upload) - router.POST("/import/commit", h.Commit) - router.DELETE("/import/cancel", h.Cancel) -} - -// GetStatus returns current import session status. -func (h *ImportHandler) GetStatus(c *gin.Context) { - var session models.ImportSession - err := h.db.Where("status IN ?", []string{"pending", "reviewing"}). - Order("created_at DESC"). - First(&session).Error - - if err == gorm.ErrRecordNotFound { - c.JSON(http.StatusOK, gin.H{"has_pending": false}) - return - } - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "has_pending": true, - "session": session, - }) -} - -// GetPreview returns parsed hosts and conflicts for review. -func (h *ImportHandler) GetPreview(c *gin.Context) { - var session models.ImportSession - err := h.db.Where("status IN ?", []string{"pending", "reviewing"}). - Order("created_at DESC"). - First(&session).Error - - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "no pending import"}) - return - } - - var result caddy.ImportResult - if err := json.Unmarshal([]byte(session.ParsedData), &result); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse import data"}) - return - } - - // Update status to reviewing - session.Status = "reviewing" - h.db.Save(&session) - - c.JSON(http.StatusOK, result) -} - -// Upload handles manual Caddyfile upload or paste. -func (h *ImportHandler) Upload(c *gin.Context) { - var req struct { - Content string `json:"content" binding:"required"` - Filename string `json:"filename"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Create temporary file - tempPath := filepath.Join(h.importDir, fmt.Sprintf("upload-%s.caddyfile", uuid.NewString())) - if err := os.MkdirAll(h.importDir, 0755); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create import directory"}) - return - } - - if err := os.WriteFile(tempPath, []byte(req.Content), 0644); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write upload"}) - return - } - - // Process the uploaded file - if err := h.processImport(tempPath, req.Filename); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "upload processed, ready for review"}) -} - -// Commit finalizes the import with user's conflict resolutions. -func (h *ImportHandler) Commit(c *gin.Context) { - var req struct { - SessionUUID string `json:"session_uuid" binding:"required"` - Resolutions map[string]string `json:"resolutions"` // domain -> action (skip, rename, merge) - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - var session models.ImportSession - if err := h.db.Where("uuid = ? AND status = ?", req.SessionUUID, "reviewing").First(&session).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "session not found or not in reviewing state"}) - return - } - - var result caddy.ImportResult - if err := json.Unmarshal([]byte(session.ParsedData), &result); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse import data"}) - return - } - - // Convert parsed hosts to ProxyHost models - proxyHosts := caddy.ConvertToProxyHosts(result.Hosts) - - created := 0 - skipped := 0 - errors := []string{} - - for _, host := range proxyHosts { - action := req.Resolutions[host.DomainNames] - - if action == "skip" { - skipped++ - continue - } - - if action == "rename" { - host.DomainNames = host.DomainNames + "-imported" - } - - host.UUID = uuid.NewString() - - if err := h.proxyHostSvc.Create(&host); err != nil { - errors = append(errors, fmt.Sprintf("%s: %s", host.DomainNames, err.Error())) - } else { - created++ - } - } - - // Mark session as committed - now := time.Now() - session.Status = "committed" - session.CommittedAt = &now - session.UserResolutions = string(mustMarshal(req.Resolutions)) - h.db.Save(&session) - - c.JSON(http.StatusOK, gin.H{ - "created": created, - "skipped": skipped, - "errors": errors, - }) -} - -// Cancel discards a pending import session. -func (h *ImportHandler) Cancel(c *gin.Context) { - sessionUUID := c.Query("session_uuid") - if sessionUUID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "session_uuid required"}) - return - } - - var session models.ImportSession - if err := h.db.Where("uuid = ?", sessionUUID).First(&session).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "session not found"}) - return - } - - session.Status = "rejected" - h.db.Save(&session) - - c.JSON(http.StatusOK, gin.H{"message": "import cancelled"}) -} - -// processImport handles the import logic for both mounted and uploaded files. -func (h *ImportHandler) processImport(caddyfilePath, originalName string) error { - // Validate Caddy binary - if err := h.importerservice.ValidateCaddyBinary(); err != nil { - return fmt.Errorf("caddy binary not available: %w", err) - } - - // Parse and extract hosts - result, err := h.importerservice.ImportFile(caddyfilePath) - if err != nil { - return fmt.Errorf("import failed: %w", err) - } - - // Check for conflicts with existing hosts - existingHosts, _ := h.proxyHostSvc.List() - existingDomains := make(map[string]bool) - for _, host := range existingHosts { - existingDomains[host.DomainNames] = true - } - - for _, parsed := range result.Hosts { - if existingDomains[parsed.DomainNames] { - result.Conflicts = append(result.Conflicts, - fmt.Sprintf("Domain '%s' already exists in CPM+", parsed.DomainNames)) - } - } - - // Create import session - session := models.ImportSession{ - UUID: uuid.NewString(), - SourceFile: originalName, - Status: "pending", - ParsedData: string(mustMarshal(result)), - ConflictReport: string(mustMarshal(result.Conflicts)), - } - - if err := h.db.Create(&session).Error; err != nil { - return fmt.Errorf("failed to create session: %w", err) - } - - // Backup original file - if _, err := caddy.BackupCaddyfile(caddyfilePath, filepath.Join(h.importDir, "backups")); err != nil { - // Non-fatal, log and continue - fmt.Printf("Warning: failed to backup Caddyfile: %v\n", err) - } - - return nil -} - -// CheckMountedImport checks for mounted Caddyfile on startup. -func CheckMountedImport(db *gorm.DB, mountPath, caddyBinary, importDir string) error { - if _, err := os.Stat(mountPath); os.IsNotExist(err) { - return nil // No mounted file, skip - } - - // Check if already processed - var count int64 - db.Model(&models.ImportSession{}).Where("source_file = ? AND status IN ?", - mountPath, []string{"pending", "reviewing", "committed"}).Count(&count) - - if count > 0 { - return nil // Already processed - } - - handler := NewImportHandler(db, caddyBinary, importDir) - return handler.processImport(mountPath, mountPath) -} - -func mustMarshal(v interface{}) []byte { - b, _ := json.Marshal(v) - return b -} diff --git a/backend/internal/api/handlers/proxy_host_handler.go b/backend/internal/api/handlers/proxy_host_handler.go deleted file mode 100644 index eb8bbf38..00000000 --- a/backend/internal/api/handlers/proxy_host_handler.go +++ /dev/null @@ -1,121 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" -) - -// ProxyHostHandler handles CRUD operations for proxy hosts. -type ProxyHostHandler struct { - service *services.ProxyHostService -} - -// NewProxyHostHandler creates a new proxy host handler. -func NewProxyHostHandler(db *gorm.DB) *ProxyHostHandler { - return &ProxyHostHandler{ - service: services.NewProxyHostService(db), - } -} - -// RegisterRoutes registers proxy host routes. -func (h *ProxyHostHandler) RegisterRoutes(router *gin.RouterGroup) { - router.GET("/proxy-hosts", h.List) - router.POST("/proxy-hosts", h.Create) - router.GET("/proxy-hosts/:uuid", h.Get) - router.PUT("/proxy-hosts/:uuid", h.Update) - router.DELETE("/proxy-hosts/:uuid", h.Delete) -} - -// List retrieves all proxy hosts. -func (h *ProxyHostHandler) List(c *gin.Context) { - hosts, err := h.service.List() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, hosts) -} - -// Create creates a new proxy host. -func (h *ProxyHostHandler) Create(c *gin.Context) { - var host models.ProxyHost - if err := c.ShouldBindJSON(&host); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - host.UUID = uuid.NewString() - - // Assign UUIDs to locations - for i := range host.Locations { - host.Locations[i].UUID = uuid.NewString() - } - - if err := h.service.Create(&host); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, host) -} - -// Get retrieves a proxy host by UUID. -func (h *ProxyHostHandler) Get(c *gin.Context) { - uuid := c.Param("uuid") - - host, err := h.service.GetByUUID(uuid) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "proxy host not found"}) - return - } - - c.JSON(http.StatusOK, host) -} - -// Update updates an existing proxy host. -func (h *ProxyHostHandler) Update(c *gin.Context) { - uuid := c.Param("uuid") - - host, err := h.service.GetByUUID(uuid) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "proxy host not found"}) - return - } - - if err := c.ShouldBindJSON(host); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := h.service.Update(host); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, host) -} - -// Delete removes a proxy host. -func (h *ProxyHostHandler) Delete(c *gin.Context) { - uuid := c.Param("uuid") - - host, err := h.service.GetByUUID(uuid) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "proxy host not found"}) - return - } - - if err := h.service.Delete(host.ID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "proxy host deleted"}) -} diff --git a/backend/internal/api/handlers/proxy_host_handler_test.go b/backend/internal/api/handlers/proxy_host_handler_test.go deleted file mode 100644 index e9ca44ba..00000000 --- a/backend/internal/api/handlers/proxy_host_handler_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/require" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) { - t.Helper() - - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) - require.NoError(t, err) - require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{})) - - h := NewProxyHostHandler(db) - r := gin.New() - api := r.Group("/api/v1") - h.RegisterRoutes(api) - - return r, db -} - -func TestProxyHostLifecycle(t *testing.T) { - router, _ := setupTestRouter(t) - - body := `{"name":"Media","domain_names":"media.example.com","forward_scheme":"http","forward_host":"media","forward_port":32400,"enabled":true}` - req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts", strings.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - - resp := httptest.NewRecorder() - router.ServeHTTP(resp, req) - require.Equal(t, http.StatusCreated, resp.Code) - - var created models.ProxyHost - require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &created)) - require.Equal(t, "media.example.com", created.DomainNames) - - listReq := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts", nil) - listResp := httptest.NewRecorder() - router.ServeHTTP(listResp, listReq) - require.Equal(t, http.StatusOK, listResp.Code) - - var hosts []models.ProxyHost - require.NoError(t, json.Unmarshal(listResp.Body.Bytes(), &hosts)) - require.Len(t, hosts, 1) - - // Get by ID - getReq := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts/"+created.UUID, nil) - getResp := httptest.NewRecorder() - router.ServeHTTP(getResp, getReq) - require.Equal(t, http.StatusOK, getResp.Code) - - var fetched models.ProxyHost - require.NoError(t, json.Unmarshal(getResp.Body.Bytes(), &fetched)) - require.Equal(t, created.UUID, fetched.UUID) - - // Update - updateBody := `{"name":"Media Updated","domain_names":"media.example.com","forward_scheme":"http","forward_host":"media","forward_port":32400,"enabled":false}` - updateReq := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+created.UUID, strings.NewReader(updateBody)) - updateReq.Header.Set("Content-Type", "application/json") - updateResp := httptest.NewRecorder() - router.ServeHTTP(updateResp, updateReq) - require.Equal(t, http.StatusOK, updateResp.Code) - - var updated models.ProxyHost - require.NoError(t, json.Unmarshal(updateResp.Body.Bytes(), &updated)) - require.Equal(t, "Media Updated", updated.Name) - require.False(t, updated.Enabled) - - // Delete - delReq := httptest.NewRequest(http.MethodDelete, "/api/v1/proxy-hosts/"+created.UUID, nil) - delResp := httptest.NewRecorder() - router.ServeHTTP(delResp, delReq) - require.Equal(t, http.StatusOK, delResp.Code) - - // Verify Delete - getReq2 := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts/"+created.UUID, nil) - getResp2 := httptest.NewRecorder() - router.ServeHTTP(getResp2, getReq2) - require.Equal(t, http.StatusNotFound, getResp2.Code) -} - -func TestProxyHostErrors(t *testing.T) { - router, _ := setupTestRouter(t) - - // Get non-existent - req := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts/non-existent-uuid", nil) - resp := httptest.NewRecorder() - router.ServeHTTP(resp, req) - require.Equal(t, http.StatusNotFound, resp.Code) - - // Update non-existent - updateBody := `{"name":"Media Updated"}` - updateReq := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/non-existent-uuid", strings.NewReader(updateBody)) - updateReq.Header.Set("Content-Type", "application/json") - updateResp := httptest.NewRecorder() - router.ServeHTTP(updateResp, updateReq) - require.Equal(t, http.StatusNotFound, updateResp.Code) - - // Delete non-existent - delReq := httptest.NewRequest(http.MethodDelete, "/api/v1/proxy-hosts/non-existent-uuid", nil) - delResp := httptest.NewRecorder() - router.ServeHTTP(delResp, delReq) - require.Equal(t, http.StatusNotFound, delResp.Code) -} diff --git a/backend/internal/api/handlers/remote_server_handler.go b/backend/internal/api/handlers/remote_server_handler.go deleted file mode 100644 index 274eebc4..00000000 --- a/backend/internal/api/handlers/remote_server_handler.go +++ /dev/null @@ -1,170 +0,0 @@ -package handlers - -import ( - "fmt" - "net" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" -) - -// RemoteServerHandler handles HTTP requests for remote server management. -type RemoteServerHandler struct { - service *services.RemoteServerService -} - -// NewRemoteServerHandler creates a new remote server handler. -func NewRemoteServerHandler(db *gorm.DB) *RemoteServerHandler { - return &RemoteServerHandler{ - service: services.NewRemoteServerService(db), - } -} - -// RegisterRoutes registers remote server routes. -func (h *RemoteServerHandler) RegisterRoutes(router *gin.RouterGroup) { - router.GET("/remote-servers", h.List) - router.POST("/remote-servers", h.Create) - router.GET("/remote-servers/:uuid", h.Get) - router.PUT("/remote-servers/:uuid", h.Update) - router.DELETE("/remote-servers/:uuid", h.Delete) - router.POST("/remote-servers/:uuid/test", h.TestConnection) -} - -// List retrieves all remote servers. -func (h *RemoteServerHandler) List(c *gin.Context) { - enabledOnly := c.Query("enabled") == "true" - - servers, err := h.service.List(enabledOnly) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, servers) -} - -// Create creates a new remote server. -func (h *RemoteServerHandler) Create(c *gin.Context) { - var server models.RemoteServer - if err := c.ShouldBindJSON(&server); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - server.UUID = uuid.NewString() - - if err := h.service.Create(&server); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, server) -} - -// Get retrieves a remote server by UUID. -func (h *RemoteServerHandler) Get(c *gin.Context) { - uuid := c.Param("uuid") - - server, err := h.service.GetByUUID(uuid) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) - return - } - - c.JSON(http.StatusOK, server) -} - -// Update updates an existing remote server. -func (h *RemoteServerHandler) Update(c *gin.Context) { - uuid := c.Param("uuid") - - server, err := h.service.GetByUUID(uuid) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) - return - } - - if err := c.ShouldBindJSON(server); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := h.service.Update(server); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, server) -} - -// Delete removes a remote server. -func (h *RemoteServerHandler) Delete(c *gin.Context) { - uuid := c.Param("uuid") - - server, err := h.service.GetByUUID(uuid) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) - return - } - - if err := h.service.Delete(server.ID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusNoContent, nil) -} - -// TestConnection tests the TCP connection to a remote server. -func (h *RemoteServerHandler) TestConnection(c *gin.Context) { - uuid := c.Param("uuid") - - server, err := h.service.GetByUUID(uuid) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) - return - } - - // Test TCP connection with 5 second timeout - address := net.JoinHostPort(server.Host, fmt.Sprintf("%d", server.Port)) - conn, err := net.DialTimeout("tcp", address, 5*time.Second) - - result := gin.H{ - "server_uuid": server.UUID, - "address": address, - "timestamp": time.Now().UTC(), - } - - if err != nil { - result["reachable"] = false - result["error"] = err.Error() - - // Update server reachability status - server.Reachable = false - now := time.Now().UTC() - server.LastChecked = &now - h.service.Update(server) - - c.JSON(http.StatusOK, result) - return - } - defer conn.Close() - - // Connection successful - result["reachable"] = true - result["latency_ms"] = time.Since(time.Now()).Milliseconds() - - // Update server reachability status - server.Reachable = true - now := time.Now().UTC() - server.LastChecked = &now - h.service.Update(server) - - c.JSON(http.StatusOK, result) -} diff --git a/backend/internal/api/handlers/user_handler.go b/backend/internal/api/handlers/user_handler.go deleted file mode 100644 index c7a7473b..00000000 --- a/backend/internal/api/handlers/user_handler.go +++ /dev/null @@ -1,113 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -type UserHandler struct { - DB *gorm.DB -} - -func NewUserHandler(db *gorm.DB) *UserHandler { - return &UserHandler{DB: db} -} - -func (h *UserHandler) RegisterRoutes(r *gin.RouterGroup) { - r.GET("/setup", h.GetSetupStatus) - r.POST("/setup", h.Setup) -} - -// GetSetupStatus checks if the application needs initial setup (i.e., no users exist). -func (h *UserHandler) GetSetupStatus(c *gin.Context) { - var count int64 - if err := h.DB.Model(&models.User{}).Count(&count).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check setup status"}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "setupRequired": count == 0, - }) -} - -type SetupRequest struct { - Name string `json:"name" binding:"required"` - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=8"` -} - -// Setup creates the initial admin user and configures the ACME email. -func (h *UserHandler) Setup(c *gin.Context) { - // 1. Check if setup is allowed - var count int64 - if err := h.DB.Model(&models.User{}).Count(&count).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check setup status"}) - return - } - - if count > 0 { - c.JSON(http.StatusForbidden, gin.H{"error": "Setup already completed"}) - return - } - - // 2. Parse request - var req SetupRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // 3. Create User - user := models.User{ - UUID: uuid.New().String(), - Name: req.Name, - Email: req.Email, - Role: "admin", - Enabled: true, - } - - if err := user.SetPassword(req.Password); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) - return - } - - // 4. Create Setting for ACME Email - acmeEmailSetting := models.Setting{ - Key: "caddy.acme_email", - Value: req.Email, - Type: "string", - Category: "caddy", - } - - // Transaction to ensure both succeed - err := h.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Create(&user).Error; err != nil { - return err - } - // Use Save to update if exists (though it shouldn't in fresh setup) or create - if err := tx.Where(models.Setting{Key: "caddy.acme_email"}).Assign(models.Setting{Value: req.Email}).FirstOrCreate(&acmeEmailSetting).Error; err != nil { - return err - } - return nil - }) - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to complete setup: " + err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{ - "message": "Setup completed successfully", - "user": gin.H{ - "id": user.ID, - "email": user.Email, - "name": user.Name, - }, - }) -} diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go deleted file mode 100644 index 877f4132..00000000 --- a/backend/internal/api/middleware/auth.go +++ /dev/null @@ -1,55 +0,0 @@ -package middleware - -import ( - "net/http" - "strings" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" - "github.com/gin-gonic/gin" -) - -func AuthMiddleware(authService *services.AuthService) gin.HandlerFunc { - return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - // Try cookie - cookie, err := c.Cookie("auth_token") - if err == nil { - authHeader = "Bearer " + cookie - } - } - - if authHeader == "" { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) - return - } - - tokenString := strings.TrimPrefix(authHeader, "Bearer ") - claims, err := authService.ValidateToken(tokenString) - if err != nil { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) - return - } - - c.Set("userID", claims.UserID) - c.Set("role", claims.Role) - c.Next() - } -} - -func RequireRole(role string) gin.HandlerFunc { - return func(c *gin.Context) { - userRole, exists := c.Get("role") - if !exists { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - if userRole.(string) != role && userRole.(string) != "admin" { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden"}) - return - } - - c.Next() - } -} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go deleted file mode 100644 index a059e9ee..00000000 --- a/backend/internal/api/routes/routes.go +++ /dev/null @@ -1,77 +0,0 @@ -package routes - -import ( - "fmt" - - "github.com/gin-gonic/gin" - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/middleware" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" -) - -// Register wires up API routes and performs automatic migrations. -func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { - // AutoMigrate all models for Issue #5 persistence layer - if err := db.AutoMigrate( - &models.ProxyHost{}, - &models.Location{}, - &models.CaddyConfig{}, - &models.RemoteServer{}, - &models.SSLCertificate{}, - &models.AccessList{}, - &models.User{}, - &models.Setting{}, - &models.ImportSession{}, - ); err != nil { - return fmt.Errorf("auto migrate: %w", err) - } - - router.GET("/api/v1/health", handlers.HealthHandler) - - api := router.Group("/api/v1") - - // Auth routes - authService := services.NewAuthService(db, cfg) - authHandler := handlers.NewAuthHandler(authService) - authMiddleware := middleware.AuthMiddleware(authService) - - api.POST("/auth/login", authHandler.Login) - api.POST("/auth/register", authHandler.Register) - - protected := api.Group("/") - protected.Use(authMiddleware) - { - protected.POST("/auth/logout", authHandler.Logout) - protected.GET("/auth/me", authHandler.Me) - protected.POST("/auth/change-password", authHandler.ChangePassword) - } - - proxyHostHandler := handlers.NewProxyHostHandler(db) - proxyHostHandler.RegisterRoutes(api) - - remoteServerHandler := handlers.NewRemoteServerHandler(db) - remoteServerHandler.RegisterRoutes(api) - - userHandler := handlers.NewUserHandler(db) - userHandler.RegisterRoutes(api) - - // Certificate routes - // Use cfg.CaddyConfigDir + "/data" for cert service - caddyDataDir := cfg.CaddyConfigDir + "/data" - certService := services.NewCertificateService(caddyDataDir) - certHandler := handlers.NewCertificateHandler(certService) - api.GET("/certificates", certHandler.List) - - return nil -} - -// RegisterImportHandler wires up import routes with config dependencies. -func RegisterImportHandler(router *gin.Engine, db *gorm.DB, caddyBinary, importDir string) { - importHandler := handlers.NewImportHandler(db, caddyBinary, importDir) - api := router.Group("/api/v1") - importHandler.RegisterRoutes(api) -} diff --git a/backend/internal/caddy/client.go b/backend/internal/caddy/client.go deleted file mode 100644 index c6408116..00000000 --- a/backend/internal/caddy/client.go +++ /dev/null @@ -1,101 +0,0 @@ -package caddy - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -// Client wraps the Caddy admin API. -type Client struct { - baseURL string - httpClient *http.Client -} - -// NewClient creates a Caddy API client. -func NewClient(adminAPIURL string) *Client { - return &Client{ - baseURL: adminAPIURL, - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, - } -} - -// Load atomically replaces Caddy's entire configuration. -// This is the primary method for applying configuration changes. -func (c *Client) Load(ctx context.Context, config *Config) error { - body, err := json.Marshal(config) - if err != nil { - return fmt.Errorf("marshal config: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/load", bytes.NewReader(body)) - if err != nil { - return fmt.Errorf("create request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("execute request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - return fmt.Errorf("caddy returned status %d: %s", resp.StatusCode, string(bodyBytes)) - } - - return nil -} - -// GetConfig retrieves the current running configuration from Caddy. -func (c *Client) GetConfig(ctx context.Context) (*Config, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/config/", nil) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("execute request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("caddy returned status %d: %s", resp.StatusCode, string(bodyBytes)) - } - - var config Config - if err := json.NewDecoder(resp.Body).Decode(&config); err != nil { - return nil, fmt.Errorf("decode response: %w", err) - } - - return &config, nil -} - -// Ping checks if Caddy admin API is reachable. -func (c *Client) Ping(ctx context.Context) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/config/", nil) - if err != nil { - return fmt.Errorf("create request: %w", err) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("caddy unreachable: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("caddy returned status %d", resp.StatusCode) - } - - return nil -} diff --git a/backend/internal/caddy/client_test.go b/backend/internal/caddy/client_test.go deleted file mode 100644 index 7e88857e..00000000 --- a/backend/internal/caddy/client_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package caddy - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -func TestClient_Load_Success(t *testing.T) { - // Mock Caddy admin API - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, "/load", r.URL.Path) - require.Equal(t, http.MethodPost, r.Method) - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - client := NewClient(server.URL) - config, _ := GenerateConfig([]models.ProxyHost{ - { - UUID: "test", - DomainNames: "test.com", - ForwardHost: "app", - ForwardPort: 8080, - Enabled: true, - }, - }, "/tmp/caddy-data", "admin@example.com") - - err := client.Load(context.Background(), config) - require.NoError(t, err) -} - -func TestClient_Load_Failure(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error": "invalid config"}`)) - })) - defer server.Close() - - client := NewClient(server.URL) - config := &Config{} - - err := client.Load(context.Background(), config) - require.Error(t, err) - require.Contains(t, err.Error(), "400") -} - -func TestClient_GetConfig_Success(t *testing.T) { - testConfig := &Config{ - Apps: Apps{ - HTTP: &HTTPApp{ - Servers: map[string]*Server{ - "test": {Listen: []string{":80"}}, - }, - }, - }, - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, "/config/", r.URL.Path) - require.Equal(t, http.MethodGet, r.Method) - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(testConfig) - })) - defer server.Close() - - client := NewClient(server.URL) - config, err := client.GetConfig(context.Background()) - require.NoError(t, err) - require.NotNil(t, config) - require.NotNil(t, config.Apps.HTTP) -} - -func TestClient_Ping_Success(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - client := NewClient(server.URL) - err := client.Ping(context.Background()) - require.NoError(t, err) -} - -func TestClient_Ping_Unreachable(t *testing.T) { - client := NewClient("http://localhost:9999") - err := client.Ping(context.Background()) - require.Error(t, err) -} diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go deleted file mode 100644 index ae479556..00000000 --- a/backend/internal/caddy/config.go +++ /dev/null @@ -1,129 +0,0 @@ -package caddy - -import ( - "fmt" - "strings" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -// GenerateConfig creates a Caddy JSON configuration from proxy hosts. -// This is the core transformation layer from our database model to Caddy config. -func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string) (*Config, error) { - config := &Config{ - Apps: Apps{ - HTTP: &HTTPApp{ - Servers: map[string]*Server{}, - }, - }, - Storage: Storage{ - System: "file_system", - Root: storageDir, - }, - } - - if acmeEmail != "" { - config.Apps.TLS = &TLSApp{ - Automation: &AutomationConfig{ - Policies: []*AutomationPolicy{ - { - IssuersRaw: []interface{}{ - map[string]interface{}{ - "module": "acme", - "email": acmeEmail, - }, - map[string]interface{}{ - "module": "zerossl", - "email": acmeEmail, - }, - }, - }, - }, - }, - } - } - - if len(hosts) == 0 { - return config, nil - } - - routes := make([]*Route, 0) - - for _, host := range hosts { - if !host.Enabled { - continue - } - - if host.DomainNames == "" { - return nil, fmt.Errorf("proxy host %s has empty domain names", host.UUID) - } - - // Parse comma-separated domains - domains := strings.Split(host.DomainNames, ",") - for i := range domains { - domains[i] = strings.TrimSpace(domains[i]) - } - - // Build handlers for this host - handlers := make([]Handler, 0) - - // Add HSTS header if enabled - if host.HSTSEnabled { - hstsValue := "max-age=31536000" - if host.HSTSSubdomains { - hstsValue += "; includeSubDomains" - } - handlers = append(handlers, HeaderHandler(map[string][]string{ - "Strict-Transport-Security": {hstsValue}, - })) - } - - // Add exploit blocking if enabled - if host.BlockExploits { - handlers = append(handlers, BlockExploitsHandler()) - } - - // Handle custom locations first (more specific routes) - for _, loc := range host.Locations { - dial := fmt.Sprintf("%s:%d", loc.ForwardHost, loc.ForwardPort) - locRoute := &Route{ - Match: []Match{ - { - Host: domains, - Path: []string{loc.Path, loc.Path + "/*"}, - }, - }, - Handle: []Handler{ - ReverseProxyHandler(dial, host.WebsocketSupport), - }, - Terminal: true, - } - routes = append(routes, locRoute) - } - - // Main proxy handler - dial := fmt.Sprintf("%s:%d", host.ForwardHost, host.ForwardPort) - mainHandlers := append(handlers, ReverseProxyHandler(dial, host.WebsocketSupport)) - - route := &Route{ - Match: []Match{ - {Host: domains}, - }, - Handle: mainHandlers, - Terminal: true, - } - - routes = append(routes, route) - } - - config.Apps.HTTP.Servers["cpm_server"] = &Server{ - Listen: []string{":80", ":443"}, - Routes: routes, - AutoHTTPS: &AutoHTTPSConfig{ - Disable: false, - DisableRedir: false, - }, - } - - return config, nil -} diff --git a/backend/internal/caddy/config_test.go b/backend/internal/caddy/config_test.go deleted file mode 100644 index e537504f..00000000 --- a/backend/internal/caddy/config_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package caddy - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -func TestGenerateConfig_Empty(t *testing.T) { - config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com") - require.NoError(t, err) - require.NotNil(t, config) - require.NotNil(t, config.Apps.HTTP) - require.Empty(t, config.Apps.HTTP.Servers) -} - -func TestGenerateConfig_SingleHost(t *testing.T) { - hosts := []models.ProxyHost{ - { - UUID: "test-uuid", - Name: "Media", - DomainNames: "media.example.com", - ForwardScheme: "http", - ForwardHost: "media", - ForwardPort: 32400, - SSLForced: true, - WebsocketSupport: false, - Enabled: true, - }, - } - - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") - require.NoError(t, err) - require.NotNil(t, config) - require.NotNil(t, config.Apps.HTTP) - require.Len(t, config.Apps.HTTP.Servers, 1) - - server := config.Apps.HTTP.Servers["cpm_server"] - require.NotNil(t, server) - require.Contains(t, server.Listen, ":80") - require.Contains(t, server.Listen, ":443") - require.Len(t, server.Routes, 1) - - route := server.Routes[0] - require.Len(t, route.Match, 1) - require.Equal(t, []string{"media.example.com"}, route.Match[0].Host) - require.Len(t, route.Handle, 1) - require.True(t, route.Terminal) - - handler := route.Handle[0] - require.Equal(t, "reverse_proxy", handler["handler"]) -} - -func TestGenerateConfig_MultipleHosts(t *testing.T) { - hosts := []models.ProxyHost{ - { - UUID: "uuid-1", - DomainNames: "site1.example.com", - ForwardHost: "app1", - ForwardPort: 8080, - Enabled: true, - }, - { - UUID: "uuid-2", - DomainNames: "site2.example.com", - ForwardHost: "app2", - ForwardPort: 8081, - Enabled: true, - }, - } - - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") - require.NoError(t, err) - require.Len(t, config.Apps.HTTP.Servers["cpm_server"].Routes, 2) -} - -func TestGenerateConfig_WebSocketEnabled(t *testing.T) { - hosts := []models.ProxyHost{ - { - UUID: "uuid-ws", - DomainNames: "ws.example.com", - ForwardHost: "wsapp", - ForwardPort: 3000, - WebsocketSupport: true, - Enabled: true, - }, - } - - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") - require.NoError(t, err) - - route := config.Apps.HTTP.Servers["cpm_server"].Routes[0] - handler := route.Handle[0] - - // Check WebSocket headers are present - require.NotNil(t, handler["headers"]) -} - -func TestGenerateConfig_EmptyDomain(t *testing.T) { - hosts := []models.ProxyHost{ - { - UUID: "bad-uuid", - DomainNames: "", - ForwardHost: "app", - ForwardPort: 8080, - Enabled: true, - }, - } - - _, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") - require.Error(t, err) - require.Contains(t, err.Error(), "empty domain") -} diff --git a/backend/internal/caddy/importer.go b/backend/internal/caddy/importer.go deleted file mode 100644 index e8c95a8b..00000000 --- a/backend/internal/caddy/importer.go +++ /dev/null @@ -1,263 +0,0 @@ -package caddy - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -// CaddyConfig represents the root structure of Caddy's JSON config. -type CaddyConfig struct { - Apps *CaddyApps `json:"apps,omitempty"` -} - -// CaddyApps contains application-specific configurations. -type CaddyApps struct { - HTTP *CaddyHTTP `json:"http,omitempty"` -} - -// CaddyHTTP represents the HTTP app configuration. -type CaddyHTTP struct { - Servers map[string]*CaddyServer `json:"servers,omitempty"` -} - -// CaddyServer represents a single server configuration. -type CaddyServer struct { - Routes []*CaddyRoute `json:"routes,omitempty"` - TLSConnectionPolicies interface{} `json:"tls_connection_policies,omitempty"` -} - -// CaddyRoute represents a single route with matchers and handlers. -type CaddyRoute struct { - Match []*CaddyMatcher `json:"match,omitempty"` - Handle []*CaddyHandler `json:"handle,omitempty"` -} - -// CaddyMatcher represents route matching criteria. -type CaddyMatcher struct { - Host []string `json:"host,omitempty"` -} - -// CaddyHandler represents a handler in the route. -type CaddyHandler struct { - Handler string `json:"handler"` - Upstreams interface{} `json:"upstreams,omitempty"` - Headers interface{} `json:"headers,omitempty"` -} - -// ParsedHost represents a single host detected during Caddyfile import. -type ParsedHost struct { - DomainNames string `json:"domain_names"` - ForwardScheme string `json:"forward_scheme"` - ForwardHost string `json:"forward_host"` - ForwardPort int `json:"forward_port"` - SSLForced bool `json:"ssl_forced"` - WebsocketSupport bool `json:"websocket_support"` - RawJSON string `json:"raw_json"` // Original Caddy JSON for this route - Warnings []string `json:"warnings"` // Unsupported features -} - -// ImportResult contains parsed hosts and detected conflicts. -type ImportResult struct { - Hosts []ParsedHost `json:"hosts"` - Conflicts []string `json:"conflicts"` - Errors []string `json:"errors"` -} - -// Importer handles Caddyfile parsing and conversion to CPM+ models. -type Importer struct { - caddyBinaryPath string -} - -// NewImporter creates a new Caddyfile importer. -func NewImporter(binaryPath string) *Importer { - if binaryPath == "" { - binaryPath = "caddy" // Default to PATH - } - return &Importer{caddyBinaryPath: binaryPath} -} - -// ParseCaddyfile reads a Caddyfile and converts it to Caddy JSON. -func (i *Importer) ParseCaddyfile(caddyfilePath string) ([]byte, error) { - if _, err := os.Stat(caddyfilePath); os.IsNotExist(err) { - return nil, fmt.Errorf("caddyfile not found: %s", caddyfilePath) - } - - cmd := exec.Command(i.caddyBinaryPath, "adapt", "--config", caddyfilePath, "--adapter", "caddyfile") - output, err := cmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("caddy adapt failed: %w (output: %s)", err, string(output)) - } - - return output, nil -} - -// ExtractHosts parses Caddy JSON and extracts proxy host information. -func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) { - var config CaddyConfig - if err := json.Unmarshal(caddyJSON, &config); err != nil { - return nil, fmt.Errorf("parsing caddy json: %w", err) - } - - result := &ImportResult{ - Hosts: []ParsedHost{}, - Conflicts: []string{}, - Errors: []string{}, - } - - if config.Apps == nil || config.Apps.HTTP == nil || config.Apps.HTTP.Servers == nil { - return result, nil // Empty config - } - - seenDomains := make(map[string]bool) - - for serverName, server := range config.Apps.HTTP.Servers { - for routeIdx, route := range server.Routes { - for _, match := range route.Match { - for _, hostMatcher := range match.Host { - domain := hostMatcher - - // Check for duplicate domains - if seenDomains[domain] { - result.Conflicts = append(result.Conflicts, - fmt.Sprintf("Duplicate domain detected: %s", domain)) - continue - } - seenDomains[domain] = true - - // Extract reverse proxy handler - host := ParsedHost{ - DomainNames: domain, - SSLForced: strings.HasPrefix(domain, "https") || server.TLSConnectionPolicies != nil, - } - - // Find reverse_proxy handler - for _, handler := range route.Handle { - if handler.Handler == "reverse_proxy" { - upstreams, _ := handler.Upstreams.([]interface{}) - if len(upstreams) > 0 { - if upstream, ok := upstreams[0].(map[string]interface{}); ok { - dial, _ := upstream["dial"].(string) - if dial != "" { - parts := strings.Split(dial, ":") - if len(parts) == 2 { - host.ForwardHost = parts[0] - fmt.Sscanf(parts[1], "%d", &host.ForwardPort) - } - } - } - } - - // Check for websocket support - if headers, ok := handler.Headers.(map[string]interface{}); ok { - if upgrade, ok := headers["Upgrade"].([]interface{}); ok { - for _, v := range upgrade { - if v == "websocket" { - host.WebsocketSupport = true - break - } - } - } - } - - // Default scheme - host.ForwardScheme = "http" - if host.SSLForced { - host.ForwardScheme = "https" - } - } - - // Detect unsupported features - if handler.Handler == "rewrite" { - host.Warnings = append(host.Warnings, "Rewrite rules not supported - manual configuration required") - } - if handler.Handler == "file_server" { - host.Warnings = append(host.Warnings, "File server directives not supported") - } - } - - // Store raw JSON for this route - routeJSON, _ := json.Marshal(map[string]interface{}{ - "server": serverName, - "route": routeIdx, - "data": route, - }) - host.RawJSON = string(routeJSON) - - result.Hosts = append(result.Hosts, host) - } - } - } - } - - return result, nil -} - -// ImportFile performs complete import: parse Caddyfile and extract hosts. -func (i *Importer) ImportFile(caddyfilePath string) (*ImportResult, error) { - caddyJSON, err := i.ParseCaddyfile(caddyfilePath) - if err != nil { - return nil, err - } - - return i.ExtractHosts(caddyJSON) -} - -// ConvertToProxyHosts converts parsed hosts to ProxyHost models. -func ConvertToProxyHosts(parsedHosts []ParsedHost) []models.ProxyHost { - hosts := make([]models.ProxyHost, 0, len(parsedHosts)) - - for _, parsed := range parsedHosts { - if parsed.ForwardHost == "" || parsed.ForwardPort == 0 { - continue // Skip invalid entries - } - - hosts = append(hosts, models.ProxyHost{ - Name: parsed.DomainNames, // Can be customized by user during review - DomainNames: parsed.DomainNames, - ForwardScheme: parsed.ForwardScheme, - ForwardHost: parsed.ForwardHost, - ForwardPort: parsed.ForwardPort, - SSLForced: parsed.SSLForced, - WebsocketSupport: parsed.WebsocketSupport, - }) - } - - return hosts -} - -// ValidateCaddyBinary checks if the Caddy binary is available. -func (i *Importer) ValidateCaddyBinary() error { - cmd := exec.Command(i.caddyBinaryPath, "version") - if err := cmd.Run(); err != nil { - return errors.New("caddy binary not found or not executable") - } - return nil -} - -// BackupCaddyfile creates a timestamped backup of the original Caddyfile. -func BackupCaddyfile(originalPath, backupDir string) (string, error) { - if err := os.MkdirAll(backupDir, 0755); err != nil { - return "", fmt.Errorf("creating backup directory: %w", err) - } - - timestamp := fmt.Sprintf("%d", os.Getpid()) // Simple timestamp placeholder - backupPath := filepath.Join(backupDir, fmt.Sprintf("Caddyfile.%s.backup", timestamp)) - - input, err := os.ReadFile(originalPath) - if err != nil { - return "", fmt.Errorf("reading original file: %w", err) - } - - if err := os.WriteFile(backupPath, input, 0644); err != nil { - return "", fmt.Errorf("writing backup: %w", err) - } - - return backupPath, nil -} diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go deleted file mode 100644 index e2ade3b4..00000000 --- a/backend/internal/caddy/manager.go +++ /dev/null @@ -1,206 +0,0 @@ -package caddy - -import ( - "context" - "crypto/sha256" - "encoding/json" - "fmt" - "os" - "path/filepath" - "sort" - "time" - - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -// Manager orchestrates Caddy configuration lifecycle: generate, validate, apply, rollback. -type Manager struct { - client *Client - db *gorm.DB - configDir string -} - -// NewManager creates a configuration manager. -func NewManager(client *Client, db *gorm.DB, configDir string) *Manager { - return &Manager{ - client: client, - db: db, - configDir: configDir, - } -} - -// ApplyConfig generates configuration from database, validates it, applies to Caddy with rollback on failure. -func (m *Manager) ApplyConfig(ctx context.Context) error { - // Fetch all proxy hosts from database - var hosts []models.ProxyHost - if err := m.db.Find(&hosts).Error; err != nil { - return fmt.Errorf("fetch proxy hosts: %w", err) - } - - // Fetch ACME email setting - var acmeEmailSetting models.Setting - var acmeEmail string - if err := m.db.Where("key = ?", "caddy.acme_email").First(&acmeEmailSetting).Error; err == nil { - acmeEmail = acmeEmailSetting.Value - } - - // Generate Caddy config - config, err := GenerateConfig(hosts, filepath.Join(m.configDir, "data"), acmeEmail) - if err != nil { - return fmt.Errorf("generate config: %w", err) - } - - // Validate before applying - if err := Validate(config); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Save snapshot for rollback - if _, err := m.saveSnapshot(config); err != nil { - return fmt.Errorf("save snapshot: %w", err) - } - - // Calculate config hash for audit trail - configJSON, _ := json.Marshal(config) - configHash := fmt.Sprintf("%x", sha256.Sum256(configJSON)) - - // Apply to Caddy - if err := m.client.Load(ctx, config); err != nil { - // Rollback on failure - if rollbackErr := m.rollback(ctx); rollbackErr != nil { - return fmt.Errorf("apply failed: %w, rollback also failed: %v", err, rollbackErr) - } - - // Record failed attempt - m.recordConfigChange(configHash, false, err.Error()) - return fmt.Errorf("apply failed (rolled back): %w", err) - } - - // Record successful application - m.recordConfigChange(configHash, true, "") - - // Cleanup old snapshots (keep last 10) - if err := m.rotateSnapshots(10); err != nil { - // Non-fatal - log but don't fail - fmt.Printf("warning: snapshot rotation failed: %v\n", err) - } - - return nil -} - -// saveSnapshot stores the config to disk with timestamp. -func (m *Manager) saveSnapshot(config *Config) (string, error) { - timestamp := time.Now().Unix() - filename := fmt.Sprintf("config-%d.json", timestamp) - path := filepath.Join(m.configDir, filename) - - configJSON, err := json.MarshalIndent(config, "", " ") - if err != nil { - return "", fmt.Errorf("marshal config: %w", err) - } - - if err := os.WriteFile(path, configJSON, 0644); err != nil { - return "", fmt.Errorf("write snapshot: %w", err) - } - - return path, nil -} - -// rollback loads the most recent snapshot from disk. -func (m *Manager) rollback(ctx context.Context) error { - snapshots, err := m.listSnapshots() - if err != nil || len(snapshots) == 0 { - return fmt.Errorf("no snapshots available for rollback") - } - - // Load most recent snapshot - latestSnapshot := snapshots[len(snapshots)-1] - configJSON, err := os.ReadFile(latestSnapshot) - if err != nil { - return fmt.Errorf("read snapshot: %w", err) - } - - var config Config - if err := json.Unmarshal(configJSON, &config); err != nil { - return fmt.Errorf("unmarshal snapshot: %w", err) - } - - // Apply the snapshot - if err := m.client.Load(ctx, &config); err != nil { - return fmt.Errorf("load snapshot: %w", err) - } - - return nil -} - -// listSnapshots returns all snapshot file paths sorted by modification time. -func (m *Manager) listSnapshots() ([]string, error) { - entries, err := os.ReadDir(m.configDir) - if err != nil { - return nil, fmt.Errorf("read config dir: %w", err) - } - - var snapshots []string - for _, entry := range entries { - if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { - continue - } - snapshots = append(snapshots, filepath.Join(m.configDir, entry.Name())) - } - - // Sort by modification time - sort.Slice(snapshots, func(i, j int) bool { - infoI, _ := os.Stat(snapshots[i]) - infoJ, _ := os.Stat(snapshots[j]) - return infoI.ModTime().Before(infoJ.ModTime()) - }) - - return snapshots, nil -} - -// rotateSnapshots keeps only the N most recent snapshots. -func (m *Manager) rotateSnapshots(keep int) error { - snapshots, err := m.listSnapshots() - if err != nil { - return err - } - - if len(snapshots) <= keep { - return nil - } - - // Delete oldest snapshots - toDelete := snapshots[:len(snapshots)-keep] - for _, path := range toDelete { - if err := os.Remove(path); err != nil { - return fmt.Errorf("delete snapshot %s: %w", path, err) - } - } - - return nil -} - -// recordConfigChange stores an audit record in the database. -func (m *Manager) recordConfigChange(configHash string, success bool, errorMsg string) { - record := models.CaddyConfig{ - ConfigHash: configHash, - AppliedAt: time.Now(), - Success: success, - ErrorMsg: errorMsg, - } - - // Best effort - don't fail if audit logging fails - m.db.Create(&record) -} - -// Ping checks if Caddy is reachable. -func (m *Manager) Ping(ctx context.Context) error { - return m.client.Ping(ctx) -} - -// GetCurrentConfig retrieves the running config from Caddy. -func (m *Manager) GetCurrentConfig(ctx context.Context) (*Config, error) { - return m.client.GetConfig(ctx) -} diff --git a/backend/internal/caddy/types.go b/backend/internal/caddy/types.go deleted file mode 100644 index 4cfa60c4..00000000 --- a/backend/internal/caddy/types.go +++ /dev/null @@ -1,122 +0,0 @@ -package caddy - -// Config represents Caddy's top-level JSON configuration structure. -// Reference: https://caddyserver.com/docs/json/ -type Config struct { - Apps Apps `json:"apps"` - Storage Storage `json:"storage,omitempty"` -} - -// Storage configures the storage module. -type Storage struct { - System string `json:"module"` - Root string `json:"root,omitempty"` -} - -// Apps contains all Caddy app modules. -type Apps struct { - HTTP *HTTPApp `json:"http,omitempty"` - TLS *TLSApp `json:"tls,omitempty"` -} - -// HTTPApp configures the HTTP app. -type HTTPApp struct { - Servers map[string]*Server `json:"servers"` -} - -// Server represents an HTTP server instance. -type Server struct { - Listen []string `json:"listen"` - Routes []*Route `json:"routes"` - AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"` - Logs *ServerLogs `json:"logs,omitempty"` -} - -// AutoHTTPSConfig controls automatic HTTPS behavior. -type AutoHTTPSConfig struct { - Disable bool `json:"disable,omitempty"` - DisableRedir bool `json:"disable_redirects,omitempty"` - Skip []string `json:"skip,omitempty"` -} - -// ServerLogs configures access logging. -type ServerLogs struct { - DefaultLoggerName string `json:"default_logger_name,omitempty"` -} - -// Route represents an HTTP route (matcher + handlers). -type Route struct { - Match []Match `json:"match,omitempty"` - Handle []Handler `json:"handle"` - Terminal bool `json:"terminal,omitempty"` -} - -// Match represents a request matcher. -type Match struct { - Host []string `json:"host,omitempty"` - Path []string `json:"path,omitempty"` -} - -// Handler is the interface for all handler types. -// Actual types will implement handler-specific fields. -type Handler map[string]interface{} - -// ReverseProxyHandler creates a reverse_proxy handler. -func ReverseProxyHandler(dial string, enableWS bool) Handler { - h := Handler{ - "handler": "reverse_proxy", - "upstreams": []map[string]interface{}{ - {"dial": dial}, - }, - } - - if enableWS { - // Enable WebSocket support by preserving upgrade headers - h["headers"] = map[string]interface{}{ - "request": map[string]interface{}{ - "set": map[string][]string{ - "Upgrade": {"{http.request.header.Upgrade}"}, - "Connection": {"{http.request.header.Connection}"}, - }, - }, - } - } - - return h -} - -// HeaderHandler creates a handler that sets HTTP response headers. -func HeaderHandler(headers map[string][]string) Handler { - return Handler{ - "handler": "headers", - "response": map[string]interface{}{ - "set": headers, - }, - } -} - -// BlockExploitsHandler creates a handler that blocks common exploits. -// This uses Caddy's request matchers to block malicious patterns. -func BlockExploitsHandler() Handler { - return Handler{ - "handler": "vars", - // Placeholder for future exploit blocking logic - // Can be extended with specific matchers for SQL injection, XSS, etc. - } -} - -// TLSApp configures the TLS app for certificate management. -type TLSApp struct { - Automation *AutomationConfig `json:"automation,omitempty"` -} - -// AutomationConfig controls certificate automation. -type AutomationConfig struct { - Policies []*AutomationPolicy `json:"policies,omitempty"` -} - -// AutomationPolicy defines certificate management for specific domains. -type AutomationPolicy struct { - Subjects []string `json:"subjects,omitempty"` - IssuersRaw []interface{} `json:"issuers,omitempty"` -} diff --git a/backend/internal/caddy/validator.go b/backend/internal/caddy/validator.go deleted file mode 100644 index c160afbf..00000000 --- a/backend/internal/caddy/validator.go +++ /dev/null @@ -1,146 +0,0 @@ -package caddy - -import ( - "encoding/json" - "fmt" - "net" - "strconv" - "strings" -) - -// Validate performs pre-flight validation on a Caddy config before applying it. -func Validate(cfg *Config) error { - if cfg == nil { - return fmt.Errorf("config cannot be nil") - } - - if cfg.Apps.HTTP == nil { - return nil // Empty config is valid - } - - // Track seen hosts to detect duplicates - seenHosts := make(map[string]bool) - - for serverName, server := range cfg.Apps.HTTP.Servers { - if len(server.Listen) == 0 { - return fmt.Errorf("server %s has no listen addresses", serverName) - } - - // Validate listen addresses - for _, addr := range server.Listen { - if err := validateListenAddr(addr); err != nil { - return fmt.Errorf("invalid listen address %s in server %s: %w", addr, serverName, err) - } - } - - // Validate routes - for i, route := range server.Routes { - if err := validateRoute(route, seenHosts); err != nil { - return fmt.Errorf("invalid route %d in server %s: %w", i, serverName, err) - } - } - } - - // Validate JSON marshalling works - if _, err := json.Marshal(cfg); err != nil { - return fmt.Errorf("config cannot be marshalled to JSON: %w", err) - } - - return nil -} - -func validateListenAddr(addr string) error { - // Strip network type prefix if present (tcp/, udp/) - if idx := strings.Index(addr, "/"); idx != -1 { - addr = addr[idx+1:] - } - - // Parse host:port - host, portStr, err := net.SplitHostPort(addr) - if err != nil { - return fmt.Errorf("invalid address format: %w", err) - } - - // Validate port - port, err := strconv.Atoi(portStr) - if err != nil { - return fmt.Errorf("invalid port: %w", err) - } - if port < 1 || port > 65535 { - return fmt.Errorf("port %d out of range (1-65535)", port) - } - - // Validate host (allow empty for wildcard binding) - if host != "" && net.ParseIP(host) == nil { - return fmt.Errorf("invalid IP address: %s", host) - } - - return nil -} - -func validateRoute(route *Route, seenHosts map[string]bool) error { - if len(route.Handle) == 0 { - return fmt.Errorf("route has no handlers") - } - - // Check for duplicate host matchers - for _, match := range route.Match { - for _, host := range match.Host { - if seenHosts[host] { - return fmt.Errorf("duplicate host matcher: %s", host) - } - seenHosts[host] = true - } - } - - // Validate handlers - for i, handler := range route.Handle { - if err := validateHandler(handler); err != nil { - return fmt.Errorf("invalid handler %d: %w", i, err) - } - } - - return nil -} - -func validateHandler(handler Handler) error { - handlerType, ok := handler["handler"].(string) - if !ok { - return fmt.Errorf("handler missing 'handler' field") - } - - switch handlerType { - case "reverse_proxy": - return validateReverseProxy(handler) - case "file_server", "static_response": - return nil // Accept other common handlers - default: - // Unknown handlers are allowed (Caddy is extensible) - return nil - } -} - -func validateReverseProxy(handler Handler) error { - upstreams, ok := handler["upstreams"].([]map[string]interface{}) - if !ok { - return fmt.Errorf("reverse_proxy missing upstreams") - } - - if len(upstreams) == 0 { - return fmt.Errorf("reverse_proxy has no upstreams") - } - - for i, upstream := range upstreams { - dial, ok := upstream["dial"].(string) - if !ok || dial == "" { - return fmt.Errorf("upstream %d missing dial address", i) - } - - // Validate dial address format (host:port) - if _, _, err := net.SplitHostPort(dial); err != nil { - return fmt.Errorf("upstream %d has invalid dial address %s: %w", i, dial, err) - } - } - - return nil -} diff --git a/backend/internal/caddy/validator_test.go b/backend/internal/caddy/validator_test.go deleted file mode 100644 index 477152c2..00000000 --- a/backend/internal/caddy/validator_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package caddy - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -func TestValidate_EmptyConfig(t *testing.T) { - config := &Config{} - err := Validate(config) - require.NoError(t, err) -} - -func TestValidate_ValidConfig(t *testing.T) { - hosts := []models.ProxyHost{ - { - UUID: "test", - DomainNames: "test.example.com", - ForwardHost: "10.0.1.100", - ForwardPort: 8080, - Enabled: true, - }, - } - - config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") - err := Validate(config) - require.NoError(t, err) -} - -func TestValidate_DuplicateHosts(t *testing.T) { - config := &Config{ - Apps: Apps{ - HTTP: &HTTPApp{ - Servers: map[string]*Server{ - "srv": { - Listen: []string{":80"}, - Routes: []*Route{ - { - Match: []Match{{Host: []string{"test.com"}}}, - Handle: []Handler{ - ReverseProxyHandler("app:8080", false), - }, - }, - { - Match: []Match{{Host: []string{"test.com"}}}, - Handle: []Handler{ - ReverseProxyHandler("app2:8080", false), - }, - }, - }, - }, - }, - }, - }, - } - - err := Validate(config) - require.Error(t, err) - require.Contains(t, err.Error(), "duplicate host") -} - -func TestValidate_NoListenAddresses(t *testing.T) { - config := &Config{ - Apps: Apps{ - HTTP: &HTTPApp{ - Servers: map[string]*Server{ - "srv": { - Listen: []string{}, - Routes: []*Route{}, - }, - }, - }, - }, - } - - err := Validate(config) - require.Error(t, err) - require.Contains(t, err.Error(), "no listen addresses") -} - -func TestValidate_InvalidPort(t *testing.T) { - config := &Config{ - Apps: Apps{ - HTTP: &HTTPApp{ - Servers: map[string]*Server{ - "srv": { - Listen: []string{":99999"}, - Routes: []*Route{}, - }, - }, - }, - }, - } - - err := Validate(config) - require.Error(t, err) - require.Contains(t, err.Error(), "out of range") -} - -func TestValidate_NoHandlers(t *testing.T) { - config := &Config{ - Apps: Apps{ - HTTP: &HTTPApp{ - Servers: map[string]*Server{ - "srv": { - Listen: []string{":80"}, - Routes: []*Route{ - { - Match: []Match{{Host: []string{"test.com"}}}, - Handle: []Handler{}, - }, - }, - }, - }, - }, - }, - } - - err := Validate(config) - require.Error(t, err) - require.Contains(t, err.Error(), "no handlers") -} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go deleted file mode 100644 index 74f5a633..00000000 --- a/backend/internal/config/config.go +++ /dev/null @@ -1,59 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" -) - -// Config captures runtime configuration sourced from environment variables. -type Config struct { - Environment string - HTTPPort string - DatabasePath string - FrontendDir string - CaddyAdminAPI string - CaddyConfigDir string - CaddyBinary string - ImportCaddyfile string - ImportDir string - JWTSecret string -} - -// Load reads env vars and falls back to defaults so the server can boot with zero configuration. -func Load() (Config, error) { - cfg := Config{ - Environment: getEnv("CPM_ENV", "development"), - HTTPPort: getEnv("CPM_HTTP_PORT", "8080"), - DatabasePath: getEnv("CPM_DB_PATH", filepath.Join("data", "cpm.db")), - FrontendDir: getEnv("CPM_FRONTEND_DIR", filepath.Clean(filepath.Join("..", "frontend", "dist"))), - CaddyAdminAPI: getEnv("CPM_CADDY_ADMIN_API", "http://localhost:2019"), - CaddyConfigDir: getEnv("CPM_CADDY_CONFIG_DIR", filepath.Join("data", "caddy")), - CaddyBinary: getEnv("CPM_CADDY_BINARY", "caddy"), - ImportCaddyfile: getEnv("CPM_IMPORT_CADDYFILE", "/import/Caddyfile"), - ImportDir: getEnv("CPM_IMPORT_DIR", filepath.Join("data", "imports")), - JWTSecret: getEnv("CPM_JWT_SECRET", "change-me-in-production"), - } - - if err := os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o755); err != nil { - return Config{}, fmt.Errorf("ensure data directory: %w", err) - } - - if err := os.MkdirAll(cfg.CaddyConfigDir, 0o755); err != nil { - return Config{}, fmt.Errorf("ensure caddy config directory: %w", err) - } - - if err := os.MkdirAll(cfg.ImportDir, 0o755); err != nil { - return Config{}, fmt.Errorf("ensure import directory: %w", err) - } - - return cfg, nil -} - -func getEnv(key, fallback string) string { - if val := os.Getenv(key); val != "" { - return val - } - - return fallback -} diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go deleted file mode 100644 index 2c343922..00000000 --- a/backend/internal/database/database.go +++ /dev/null @@ -1,18 +0,0 @@ -package database - -import ( - "fmt" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -// Connect opens a SQLite database connection. -func Connect(dbPath string) (*gorm.DB, error) { - db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) - if err != nil { - return nil, fmt.Errorf("open database: %w", err) - } - - return db, nil -} diff --git a/backend/internal/models/access_list.go b/backend/internal/models/access_list.go deleted file mode 100644 index 70beee11..00000000 --- a/backend/internal/models/access_list.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -import ( - "time" -) - -// AccessList defines IP-based or auth-based access control rules -// that can be applied to proxy hosts. -type AccessList struct { - ID uint `json:"id" gorm:"primaryKey"` - UUID string `json:"uuid" gorm:"uniqueIndex"` - Name string `json:"name" gorm:"index"` - Description string `json:"description"` - Type string `json:"type"` // "allow", "deny", "basic_auth", "forward_auth" - Rules string `json:"rules" gorm:"type:text"` // JSON array of rule definitions - Enabled bool `json:"enabled" gorm:"default:true"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/backend/internal/models/caddy_config.go b/backend/internal/models/caddy_config.go deleted file mode 100644 index 4b4ea08e..00000000 --- a/backend/internal/models/caddy_config.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import ( - "time" -) - -// CaddyConfig stores an audit trail of Caddy configuration changes. -type CaddyConfig struct { - ID uint `json:"id" gorm:"primaryKey"` - ConfigHash string `json:"config_hash" gorm:"index"` - AppliedAt time.Time `json:"applied_at"` - Success bool `json:"success"` - ErrorMsg string `json:"error_msg"` -} diff --git a/backend/internal/models/import_session.go b/backend/internal/models/import_session.go deleted file mode 100644 index 8ae7896a..00000000 --- a/backend/internal/models/import_session.go +++ /dev/null @@ -1,21 +0,0 @@ -package models - -import ( - "time" -) - -// ImportSession tracks Caddyfile import operations with pending state -// until user reviews and confirms via UI. -type ImportSession struct { - ID uint `json:"id" gorm:"primaryKey"` - UUID string `json:"uuid" gorm:"uniqueIndex"` - SourceFile string `json:"source_file"` // Path to original Caddyfile - Status string `json:"status" gorm:"default:'pending'"` // "pending", "reviewing", "committed", "rejected", "failed" - ParsedData string `json:"parsed_data" gorm:"type:text"` // JSON representation of detected hosts - ConflictReport string `json:"conflict_report" gorm:"type:text"` // JSON array of conflicts - UserResolutions string `json:"user_resolutions" gorm:"type:text"` // JSON map of conflict resolutions - ErrorMsg string `json:"error_msg"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - CommittedAt *time.Time `json:"committed_at,omitempty"` -} diff --git a/backend/internal/models/location.go b/backend/internal/models/location.go deleted file mode 100644 index ab05df1c..00000000 --- a/backend/internal/models/location.go +++ /dev/null @@ -1,18 +0,0 @@ -package models - -import ( - "time" -) - -// Location represents a custom path-based proxy configuration within a ProxyHost. -type Location struct { - ID uint `json:"id" gorm:"primaryKey"` - UUID string `json:"uuid" gorm:"uniqueIndex;not null"` - ProxyHostID uint `json:"proxy_host_id" gorm:"not null;index"` - Path string `json:"path" gorm:"not null"` // e.g., /api, /admin - ForwardScheme string `json:"forward_scheme" gorm:"default:http"` - ForwardHost string `json:"forward_host" gorm:"not null"` - ForwardPort int `json:"forward_port" gorm:"not null"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/backend/internal/models/proxy_host.go b/backend/internal/models/proxy_host.go deleted file mode 100644 index 268e1e37..00000000 --- a/backend/internal/models/proxy_host.go +++ /dev/null @@ -1,26 +0,0 @@ -package models - -import ( - "time" -) - -// ProxyHost represents a reverse proxy configuration. -type ProxyHost struct { - ID uint `json:"id" gorm:"primaryKey"` - UUID string `json:"uuid" gorm:"uniqueIndex;not null"` - Name string `json:"name"` - DomainNames string `json:"domain_names" gorm:"not null"` // Comma-separated list - ForwardScheme string `json:"forward_scheme" gorm:"default:http"` - ForwardHost string `json:"forward_host" gorm:"not null"` - ForwardPort int `json:"forward_port" gorm:"not null"` - SSLForced bool `json:"ssl_forced" gorm:"default:false"` - HTTP2Support bool `json:"http2_support" gorm:"default:true"` - HSTSEnabled bool `json:"hsts_enabled" gorm:"default:false"` - HSTSSubdomains bool `json:"hsts_subdomains" gorm:"default:false"` - BlockExploits bool `json:"block_exploits" gorm:"default:true"` - WebsocketSupport bool `json:"websocket_support" gorm:"default:false"` - Enabled bool `json:"enabled" gorm:"default:true"` - Locations []Location `json:"locations" gorm:"foreignKey:ProxyHostID;constraint:OnDelete:CASCADE"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/backend/internal/models/remote_server.go b/backend/internal/models/remote_server.go deleted file mode 100644 index 7619b3dc..00000000 --- a/backend/internal/models/remote_server.go +++ /dev/null @@ -1,24 +0,0 @@ -package models - -import ( - "time" -) - -// RemoteServer represents a known backend server that can be selected -// when creating proxy hosts, eliminating manual IP/port entry. -type RemoteServer struct { - ID uint `json:"id" gorm:"primaryKey"` - UUID string `json:"uuid" gorm:"uniqueIndex"` - Name string `json:"name" gorm:"index"` - Provider string `json:"provider"` // e.g., "docker", "vm", "cloud", "manual" - Host string `json:"host"` // IP address or hostname - Port int `json:"port"` - Scheme string `json:"scheme"` // http/https - Tags string `json:"tags"` // comma-separated tags for filtering - Description string `json:"description"` - Enabled bool `json:"enabled" gorm:"default:true"` - LastChecked *time.Time `json:"last_checked,omitempty"` - Reachable bool `json:"reachable" gorm:"default:false"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/backend/internal/models/setting.go b/backend/internal/models/setting.go deleted file mode 100644 index ee8b9fd6..00000000 --- a/backend/internal/models/setting.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -import ( - "time" -) - -// Setting stores global application configuration as key-value pairs. -// Used for system-wide preferences, feature flags, and runtime config. -type Setting struct { - ID uint `json:"id" gorm:"primaryKey"` - Key string `json:"key" gorm:"uniqueIndex"` - Value string `json:"value" gorm:"type:text"` - Type string `json:"type"` // "string", "int", "bool", "json" - Category string `json:"category"` // "general", "security", "caddy", "smtp", etc. - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/backend/internal/models/ssl_certificate.go b/backend/internal/models/ssl_certificate.go deleted file mode 100644 index 121368fd..00000000 --- a/backend/internal/models/ssl_certificate.go +++ /dev/null @@ -1,21 +0,0 @@ -package models - -import ( - "time" -) - -// SSLCertificate represents TLS certificates managed by CPM+. -// Can be Let's Encrypt auto-generated or custom uploaded certs. -type SSLCertificate struct { - ID uint `json:"id" gorm:"primaryKey"` - UUID string `json:"uuid" gorm:"uniqueIndex"` - Name string `json:"name"` - Provider string `json:"provider"` // "letsencrypt", "custom", "self-signed" - Domains string `json:"domains"` // comma-separated list of domains - Certificate string `json:"certificate" gorm:"type:text"` // PEM-encoded certificate - PrivateKey string `json:"private_key" gorm:"type:text"` // PEM-encoded private key - ExpiresAt *time.Time `json:"expires_at,omitempty"` - AutoRenew bool `json:"auto_renew" gorm:"default:false"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go deleted file mode 100644 index fd252dd2..00000000 --- a/backend/internal/models/user.go +++ /dev/null @@ -1,40 +0,0 @@ -package models - -import ( - "time" - - "golang.org/x/crypto/bcrypt" -) - -// User represents authenticated users with role-based access control. -// Supports local auth, SSO integration planned for later phases. -type User struct { - ID uint `json:"id" gorm:"primaryKey"` - UUID string `json:"uuid" gorm:"uniqueIndex"` - Email string `json:"email" gorm:"uniqueIndex"` - PasswordHash string `json:"-"` // Never serialize password hash - Name string `json:"name"` - Role string `json:"role" gorm:"default:'user'"` // "admin", "user", "viewer" - Enabled bool `json:"enabled" gorm:"default:true"` - FailedLoginAttempts int `json:"-" gorm:"default:0"` - LockedUntil *time.Time `json:"-"` - LastLogin *time.Time `json:"last_login,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// SetPassword hashes and sets the user's password. -func (u *User) SetPassword(password string) error { - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return err - } - u.PasswordHash = string(hash) - return nil -} - -// CheckPassword compares the provided password with the stored hash. -func (u *User) CheckPassword(password string) bool { - err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) - return err == nil -} diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go deleted file mode 100644 index e94767d6..00000000 --- a/backend/internal/server/server.go +++ /dev/null @@ -1,21 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" -) - -// NewRouter creates a new Gin router with frontend static file serving. -func NewRouter(frontendDir string) *gin.Engine { - router := gin.Default() - - // Serve frontend static files - if frontendDir != "" { - router.Static("/assets", frontendDir+"/assets") - router.StaticFile("/", frontendDir+"/index.html") - router.NoRoute(func(c *gin.Context) { - c.File(frontendDir + "/index.html") - }) - } - - return router -} diff --git a/backend/internal/services/auth_service.go b/backend/internal/services/auth_service.go deleted file mode 100644 index 3d1033b6..00000000 --- a/backend/internal/services/auth_service.go +++ /dev/null @@ -1,139 +0,0 @@ -package services - -import ( - "errors" - "time" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config" - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" - "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" - "gorm.io/gorm" -) - -type AuthService struct { - db *gorm.DB - config config.Config -} - -func NewAuthService(db *gorm.DB, cfg config.Config) *AuthService { - return &AuthService{db: db, config: cfg} -} - -type Claims struct { - UserID uint `json:"user_id"` - Role string `json:"role"` - jwt.RegisteredClaims -} - -func (s *AuthService) Register(email, password, name string) (*models.User, error) { - var count int64 - s.db.Model(&models.User{}).Count(&count) - - role := "user" - if count == 0 { - role = "admin" // First user is admin - } - - user := &models.User{ - UUID: uuid.New().String(), - Email: email, - Name: name, - Role: role, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - if err := user.SetPassword(password); err != nil { - return nil, err - } - - if err := s.db.Create(user).Error; err != nil { - return nil, err - } - - return user, nil -} - -func (s *AuthService) Login(email, password string) (string, error) { - var user models.User - if err := s.db.Where("email = ?", email).First(&user).Error; err != nil { - return "", errors.New("invalid credentials") - } - - if !user.Enabled { - return "", errors.New("account disabled") - } - - if user.LockedUntil != nil && user.LockedUntil.After(time.Now()) { - return "", errors.New("account locked") - } - - if !user.CheckPassword(password) { - user.FailedLoginAttempts++ - if user.FailedLoginAttempts >= 5 { - lockTime := time.Now().Add(15 * time.Minute) - user.LockedUntil = &lockTime - } - s.db.Save(&user) - return "", errors.New("invalid credentials") - } - - // Reset failed attempts - user.FailedLoginAttempts = 0 - user.LockedUntil = nil - now := time.Now() - user.LastLogin = &now - s.db.Save(&user) - - return s.GenerateToken(&user) -} - -func (s *AuthService) GenerateToken(user *models.User) (string, error) { - expirationTime := time.Now().Add(24 * time.Hour) - claims := &Claims{ - UserID: user.ID, - Role: user.Role, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(expirationTime), - Issuer: "cpmp", - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(s.config.JWTSecret)) -} - -func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error { - var user models.User - if err := s.db.First(&user, userID).Error; err != nil { - return errors.New("user not found") - } - - if !user.CheckPassword(oldPassword) { - return errors.New("invalid current password") - } - - if err := user.SetPassword(newPassword); err != nil { - return err - } - - return s.db.Save(&user).Error -} - -func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) { - claims := &Claims{} - token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { - return []byte(s.config.JWTSecret), nil - }) - - if err != nil { - return nil, err - } - - if !token.Valid { - return nil, errors.New("invalid token") - } - - return claims, nil -} diff --git a/backend/internal/services/certificate_service.go b/backend/internal/services/certificate_service.go deleted file mode 100644 index bee1efb6..00000000 --- a/backend/internal/services/certificate_service.go +++ /dev/null @@ -1,105 +0,0 @@ -package services - -import ( - "crypto/x509" - "encoding/pem" - "fmt" - "os" - "path/filepath" - "strings" - "time" -) - -// CertificateInfo represents parsed certificate details. -type CertificateInfo struct { - Domain string `json:"domain"` - Issuer string `json:"issuer"` - ExpiresAt time.Time `json:"expires_at"` - Status string `json:"status"` // "valid", "expiring", "expired" -} - -// CertificateService manages certificate retrieval and parsing. -type CertificateService struct { - dataDir string -} - -// NewCertificateService creates a new certificate service. -func NewCertificateService(dataDir string) *CertificateService { - return &CertificateService{ - dataDir: dataDir, - } -} - -// ListCertificates scans the Caddy data directory for certificates. -// It looks in certificates/acme-v02.api.letsencrypt.org-directory/ and others. -func (s *CertificateService) ListCertificates() ([]CertificateInfo, error) { - certs := []CertificateInfo{} - certRoot := filepath.Join(s.dataDir, "certificates") - - // Walk through the certificate directory - err := filepath.Walk(certRoot, func(path string, info os.FileInfo, err error) error { - if err != nil { - // If directory doesn't exist yet (fresh install), just return empty - if os.IsNotExist(err) { - return nil - } - return err - } - - // We only care about .crt files - if !info.IsDir() && strings.HasSuffix(info.Name(), ".crt") { - cert, err := s.parseCertificate(path) - if err != nil { - // Log error but continue scanning other certs - fmt.Printf("failed to parse cert %s: %v\n", path, err) - return nil - } - certs = append(certs, *cert) - } - return nil - }) - - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("walk certificates: %w", err) - } - - return certs, nil -} - -func (s *CertificateService) parseCertificate(path string) (*CertificateInfo, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read file: %w", err) - } - - block, _ := pem.Decode(data) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block") - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("parse certificate: %w", err) - } - - status := "valid" - now := time.Now() - if now.After(cert.NotAfter) { - status = "expired" - } else if now.Add(30 * 24 * time.Hour).After(cert.NotAfter) { - status = "expiring" - } - - // Domain is usually the CommonName or the first SAN - domain := cert.Subject.CommonName - if domain == "" && len(cert.DNSNames) > 0 { - domain = cert.DNSNames[0] - } - - return &CertificateInfo{ - Domain: domain, - Issuer: cert.Issuer.CommonName, - ExpiresAt: cert.NotAfter, - Status: status, - }, nil -} diff --git a/backend/internal/services/proxyhost_service.go b/backend/internal/services/proxyhost_service.go deleted file mode 100644 index f2328f73..00000000 --- a/backend/internal/services/proxyhost_service.go +++ /dev/null @@ -1,90 +0,0 @@ -package services - -import ( - "errors" - "fmt" - - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -// ProxyHostService encapsulates business logic for proxy host management. -type ProxyHostService struct { - db *gorm.DB -} - -// NewProxyHostService creates a new proxy host service. -func NewProxyHostService(db *gorm.DB) *ProxyHostService { - return &ProxyHostService{db: db} -} - -// ValidateUniqueDomain ensures no duplicate domains exist before creation/update. -func (s *ProxyHostService) ValidateUniqueDomain(domainNames string, excludeID uint) error { - var count int64 - query := s.db.Model(&models.ProxyHost{}).Where("domain_names = ?", domainNames) - - if excludeID > 0 { - query = query.Where("id != ?", excludeID) - } - - if err := query.Count(&count).Error; err != nil { - return fmt.Errorf("checking domain uniqueness: %w", err) - } - - if count > 0 { - return errors.New("domain already exists") - } - - return nil -} - -// Create validates and creates a new proxy host. -func (s *ProxyHostService) Create(host *models.ProxyHost) error { - if err := s.ValidateUniqueDomain(host.DomainNames, 0); err != nil { - return err - } - - return s.db.Create(host).Error -} - -// Update validates and updates an existing proxy host. -func (s *ProxyHostService) Update(host *models.ProxyHost) error { - if err := s.ValidateUniqueDomain(host.DomainNames, host.ID); err != nil { - return err - } - - return s.db.Save(host).Error -} - -// Delete removes a proxy host. -func (s *ProxyHostService) Delete(id uint) error { - return s.db.Delete(&models.ProxyHost{}, id).Error -} - -// GetByID retrieves a proxy host by ID. -func (s *ProxyHostService) GetByID(id uint) (*models.ProxyHost, error) { - var host models.ProxyHost - if err := s.db.First(&host, id).Error; err != nil { - return nil, err - } - return &host, nil -} - -// GetByUUID finds a proxy host by UUID. -func (s *ProxyHostService) GetByUUID(uuid string) (*models.ProxyHost, error) { - var host models.ProxyHost - if err := s.db.Preload("Locations").Where("uuid = ?", uuid).First(&host).Error; err != nil { - return nil, err - } - return &host, nil -} - -// List returns all proxy hosts. -func (s *ProxyHostService) List() ([]models.ProxyHost, error) { - var hosts []models.ProxyHost - if err := s.db.Preload("Locations").Order("updated_at desc").Find(&hosts).Error; err != nil { - return nil, err - } - return hosts, nil -} diff --git a/backend/internal/services/remoteserver_service.go b/backend/internal/services/remoteserver_service.go deleted file mode 100644 index b9c71c7b..00000000 --- a/backend/internal/services/remoteserver_service.go +++ /dev/null @@ -1,96 +0,0 @@ -package services - -import ( - "errors" - "fmt" - - "gorm.io/gorm" - - "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" -) - -// RemoteServerService encapsulates business logic for remote server management. -type RemoteServerService struct { - db *gorm.DB -} - -// NewRemoteServerService creates a new remote server service. -func NewRemoteServerService(db *gorm.DB) *RemoteServerService { - return &RemoteServerService{db: db} -} - -// ValidateUniqueServer ensures no duplicate name+host+port combinations. -func (s *RemoteServerService) ValidateUniqueServer(name, host string, port int, excludeID uint) error { - var count int64 - query := s.db.Model(&models.RemoteServer{}).Where("name = ? OR (host = ? AND port = ?)", name, host, port) - - if excludeID > 0 { - query = query.Where("id != ?", excludeID) - } - - if err := query.Count(&count).Error; err != nil { - return fmt.Errorf("checking server uniqueness: %w", err) - } - - if count > 0 { - return errors.New("server with same name or host:port already exists") - } - - return nil -} - -// Create validates and creates a new remote server. -func (s *RemoteServerService) Create(server *models.RemoteServer) error { - if err := s.ValidateUniqueServer(server.Name, server.Host, server.Port, 0); err != nil { - return err - } - - return s.db.Create(server).Error -} - -// Update validates and updates an existing remote server. -func (s *RemoteServerService) Update(server *models.RemoteServer) error { - if err := s.ValidateUniqueServer(server.Name, server.Host, server.Port, server.ID); err != nil { - return err - } - - return s.db.Save(server).Error -} - -// Delete removes a remote server. -func (s *RemoteServerService) Delete(id uint) error { - return s.db.Delete(&models.RemoteServer{}, id).Error -} - -// GetByID retrieves a remote server by ID. -func (s *RemoteServerService) GetByID(id uint) (*models.RemoteServer, error) { - var server models.RemoteServer - if err := s.db.First(&server, id).Error; err != nil { - return nil, err - } - return &server, nil -} - -// GetByUUID retrieves a remote server by UUID. -func (s *RemoteServerService) GetByUUID(uuid string) (*models.RemoteServer, error) { - var server models.RemoteServer - if err := s.db.Where("uuid = ?", uuid).First(&server).Error; err != nil { - return nil, err - } - return &server, nil -} - -// List retrieves all remote servers, optionally filtering by enabled status. -func (s *RemoteServerService) List(enabledOnly bool) ([]models.RemoteServer, error) { - var servers []models.RemoteServer - query := s.db - - if enabledOnly { - query = query.Where("enabled = ?", true) - } - - if err := query.Order("name ASC").Find(&servers).Error; err != nil { - return nil, err - } - return servers, nil -} diff --git a/backend/internal/version/version.go b/backend/internal/version/version.go deleted file mode 100644 index 7936ade8..00000000 --- a/backend/internal/version/version.go +++ /dev/null @@ -1,20 +0,0 @@ -package version - -const ( - // Name of the application - Name = "CPMP" - // Version is the semantic version - Version = "0.1.0" - // BuildTime is set during build via ldflags - BuildTime = "unknown" - // GitCommit is set during build via ldflags - GitCommit = "unknown" -) - -// Full returns the complete version string. -func Full() string { - if BuildTime != "unknown" && GitCommit != "unknown" { - return Version + " (commit: " + GitCommit + ", built: " + BuildTime + ")" - } - return Version -} diff --git a/backend/node_modules/.bin/esbuild b/backend/node_modules/.bin/esbuild deleted file mode 120000 index c83ac070..00000000 --- a/backend/node_modules/.bin/esbuild +++ /dev/null @@ -1 +0,0 @@ -../esbuild/bin/esbuild \ No newline at end of file diff --git a/backend/node_modules/.bin/nanoid b/backend/node_modules/.bin/nanoid deleted file mode 120000 index e2be547b..00000000 --- a/backend/node_modules/.bin/nanoid +++ /dev/null @@ -1 +0,0 @@ -../nanoid/bin/nanoid.cjs \ No newline at end of file diff --git a/backend/node_modules/.bin/parser b/backend/node_modules/.bin/parser deleted file mode 120000 index ce7bf97e..00000000 --- a/backend/node_modules/.bin/parser +++ /dev/null @@ -1 +0,0 @@ -../@babel/parser/bin/babel-parser.js \ No newline at end of file diff --git a/backend/node_modules/.bin/rollup b/backend/node_modules/.bin/rollup deleted file mode 120000 index 5939621c..00000000 --- a/backend/node_modules/.bin/rollup +++ /dev/null @@ -1 +0,0 @@ -../rollup/dist/bin/rollup \ No newline at end of file diff --git a/backend/node_modules/.bin/semver b/backend/node_modules/.bin/semver deleted file mode 120000 index 5aaadf42..00000000 --- a/backend/node_modules/.bin/semver +++ /dev/null @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/backend/node_modules/.bin/vite b/backend/node_modules/.bin/vite deleted file mode 120000 index 6d1e3bea..00000000 --- a/backend/node_modules/.bin/vite +++ /dev/null @@ -1 +0,0 @@ -../vite/bin/vite.js \ No newline at end of file diff --git a/backend/node_modules/.bin/vitest b/backend/node_modules/.bin/vitest deleted file mode 120000 index 22734979..00000000 --- a/backend/node_modules/.bin/vitest +++ /dev/null @@ -1 +0,0 @@ -../vitest/vitest.mjs \ No newline at end of file diff --git a/backend/node_modules/.bin/why-is-node-running b/backend/node_modules/.bin/why-is-node-running deleted file mode 120000 index f08a594c..00000000 --- a/backend/node_modules/.bin/why-is-node-running +++ /dev/null @@ -1 +0,0 @@ -../why-is-node-running/cli.js \ No newline at end of file