refactor: remove security-related hooks and pages

- Deleted `useSecurity.ts` hook which managed authentication users, providers, and policies.
- Removed `Policies.tsx`, `Providers.tsx`, and `Users.tsx` pages that utilized the above hook.
- Cleaned up the `index.tsx` file in the Security section to remove references to the deleted pages.
- Updated mock data by removing unused properties related to forward authentication.
This commit is contained in:
Wikid82
2025-11-26 00:02:15 +00:00
parent b20522f150
commit 4f03021c9c
37 changed files with 42 additions and 4264 deletions
@@ -1,552 +0,0 @@
package handlers
import (
"net/http"
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AuthUserHandler handles auth user operations
type AuthUserHandler struct {
db *gorm.DB
}
// NewAuthUserHandler creates a new auth user handler
func NewAuthUserHandler(db *gorm.DB) *AuthUserHandler {
return &AuthUserHandler{db: db}
}
// List returns all auth users
func (h *AuthUserHandler) List(c *gin.Context) {
var users []models.AuthUser
if err := h.db.Order("created_at desc").Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, users)
}
// Get returns a single auth user by UUID
func (h *AuthUserHandler) Get(c *gin.Context) {
uuid := c.Param("uuid")
var user models.AuthUser
if err := h.db.Where("uuid = ?", uuid).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
// CreateRequest represents the request body for creating an auth user
type CreateAuthUserRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Name string `json:"name"`
Password string `json:"password" binding:"required,min=8"`
Roles string `json:"roles"`
MFAEnabled bool `json:"mfa_enabled"`
AdditionalEmails string `json:"additional_emails"`
}
// Create creates a new auth user
func (h *AuthUserHandler) Create(c *gin.Context) {
var req CreateAuthUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := models.AuthUser{
Username: req.Username,
Email: req.Email,
Name: req.Name,
Roles: req.Roles,
MFAEnabled: req.MFAEnabled,
AdditionalEmails: req.AdditionalEmails,
Enabled: true,
}
if err := user.SetPassword(req.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
if err := h.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
// UpdateRequest represents the request body for updating an auth user
type UpdateAuthUserRequest struct {
Email *string `json:"email,omitempty"`
Name *string `json:"name,omitempty"`
Password *string `json:"password,omitempty"`
Roles *string `json:"roles,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
MFAEnabled *bool `json:"mfa_enabled,omitempty"`
AdditionalEmails *string `json:"additional_emails,omitempty"`
}
// Update updates an existing auth user
func (h *AuthUserHandler) Update(c *gin.Context) {
uuid := c.Param("uuid")
var user models.AuthUser
if err := h.db.Where("uuid = ?", uuid).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var req UpdateAuthUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Email != nil {
user.Email = *req.Email
}
if req.Name != nil {
user.Name = *req.Name
}
if req.Password != nil {
if err := user.SetPassword(*req.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
}
if req.Roles != nil {
user.Roles = *req.Roles
}
if req.Enabled != nil {
user.Enabled = *req.Enabled
}
if req.MFAEnabled != nil {
user.MFAEnabled = *req.MFAEnabled
}
if req.AdditionalEmails != nil {
user.AdditionalEmails = *req.AdditionalEmails
}
if err := h.db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
// Delete deletes an auth user
func (h *AuthUserHandler) Delete(c *gin.Context) {
uuid := c.Param("uuid")
// Prevent deletion of the last admin
var user models.AuthUser
if err := h.db.Where("uuid = ?", uuid).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Check if this is an admin user
if user.HasRole("admin") {
var adminCount int64
h.db.Model(&models.AuthUser{}).Where("roles LIKE ?", "%admin%").Count(&adminCount)
if adminCount <= 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete the last admin user"})
return
}
}
if err := h.db.Where("uuid = ?", uuid).Delete(&models.AuthUser{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
}
// Stats returns statistics about auth users
func (h *AuthUserHandler) Stats(c *gin.Context) {
var total int64
var enabled int64
var withMFA int64
h.db.Model(&models.AuthUser{}).Count(&total)
h.db.Model(&models.AuthUser{}).Where("enabled = ?", true).Count(&enabled)
h.db.Model(&models.AuthUser{}).Where("mfa_enabled = ?", true).Count(&withMFA)
c.JSON(http.StatusOK, gin.H{
"total": total,
"enabled": enabled,
"with_mfa": withMFA,
})
}
// AuthProviderHandler handles auth provider operations
type AuthProviderHandler struct {
db *gorm.DB
}
// NewAuthProviderHandler creates a new auth provider handler
func NewAuthProviderHandler(db *gorm.DB) *AuthProviderHandler {
return &AuthProviderHandler{db: db}
}
// List returns all auth providers
func (h *AuthProviderHandler) List(c *gin.Context) {
var providers []models.AuthProvider
if err := h.db.Order("created_at desc").Find(&providers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, providers)
}
// Get returns a single auth provider by UUID
func (h *AuthProviderHandler) Get(c *gin.Context) {
uuid := c.Param("uuid")
var provider models.AuthProvider
if err := h.db.Where("uuid = ?", uuid).First(&provider).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, provider)
}
// CreateProviderRequest represents the request body for creating an auth provider
type CreateProviderRequest struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
ClientID string `json:"client_id" binding:"required"`
ClientSecret string `json:"client_secret" binding:"required"`
IssuerURL string `json:"issuer_url"`
AuthURL string `json:"auth_url"`
TokenURL string `json:"token_url"`
UserInfoURL string `json:"user_info_url"`
Scopes string `json:"scopes"`
RoleMapping string `json:"role_mapping"`
IconURL string `json:"icon_url"`
DisplayName string `json:"display_name"`
}
// Create creates a new auth provider
func (h *AuthProviderHandler) Create(c *gin.Context) {
var req CreateProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
provider := models.AuthProvider{
Name: req.Name,
Type: req.Type,
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
IssuerURL: req.IssuerURL,
AuthURL: req.AuthURL,
TokenURL: req.TokenURL,
UserInfoURL: req.UserInfoURL,
Scopes: req.Scopes,
RoleMapping: req.RoleMapping,
IconURL: req.IconURL,
DisplayName: req.DisplayName,
Enabled: true,
}
if err := h.db.Create(&provider).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, provider)
}
// UpdateProviderRequest represents the request body for updating an auth provider
type UpdateProviderRequest struct {
Name *string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
ClientID *string `json:"client_id,omitempty"`
ClientSecret *string `json:"client_secret,omitempty"`
IssuerURL *string `json:"issuer_url,omitempty"`
AuthURL *string `json:"auth_url,omitempty"`
TokenURL *string `json:"token_url,omitempty"`
UserInfoURL *string `json:"user_info_url,omitempty"`
Scopes *string `json:"scopes,omitempty"`
RoleMapping *string `json:"role_mapping,omitempty"`
IconURL *string `json:"icon_url,omitempty"`
DisplayName *string `json:"display_name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
// Update updates an existing auth provider
func (h *AuthProviderHandler) Update(c *gin.Context) {
uuid := c.Param("uuid")
var provider models.AuthProvider
if err := h.db.Where("uuid = ?", uuid).First(&provider).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var req UpdateProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Name != nil {
provider.Name = *req.Name
}
if req.Type != nil {
provider.Type = *req.Type
}
if req.ClientID != nil {
provider.ClientID = *req.ClientID
}
if req.ClientSecret != nil {
provider.ClientSecret = *req.ClientSecret
}
if req.IssuerURL != nil {
provider.IssuerURL = *req.IssuerURL
}
if req.AuthURL != nil {
provider.AuthURL = *req.AuthURL
}
if req.TokenURL != nil {
provider.TokenURL = *req.TokenURL
}
if req.UserInfoURL != nil {
provider.UserInfoURL = *req.UserInfoURL
}
if req.Scopes != nil {
provider.Scopes = *req.Scopes
}
if req.RoleMapping != nil {
provider.RoleMapping = *req.RoleMapping
}
if req.IconURL != nil {
provider.IconURL = *req.IconURL
}
if req.DisplayName != nil {
provider.DisplayName = *req.DisplayName
}
if req.Enabled != nil {
provider.Enabled = *req.Enabled
}
if err := h.db.Save(&provider).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, provider)
}
// Delete deletes an auth provider
func (h *AuthProviderHandler) Delete(c *gin.Context) {
uuid := c.Param("uuid")
if err := h.db.Where("uuid = ?", uuid).Delete(&models.AuthProvider{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Provider deleted successfully"})
}
// AuthPolicyHandler handles auth policy operations
type AuthPolicyHandler struct {
db *gorm.DB
}
// NewAuthPolicyHandler creates a new auth policy handler
func NewAuthPolicyHandler(db *gorm.DB) *AuthPolicyHandler {
return &AuthPolicyHandler{db: db}
}
// List returns all auth policies
func (h *AuthPolicyHandler) List(c *gin.Context) {
var policies []models.AuthPolicy
if err := h.db.Order("created_at desc").Find(&policies).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, policies)
}
// Get returns a single auth policy by UUID
func (h *AuthPolicyHandler) Get(c *gin.Context) {
uuid := c.Param("uuid")
var policy models.AuthPolicy
if err := h.db.Where("uuid = ?", uuid).First(&policy).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, policy)
}
// GetByID returns a single auth policy by ID
func (h *AuthPolicyHandler) GetByID(id uint) (*models.AuthPolicy, error) {
var policy models.AuthPolicy
if err := h.db.First(&policy, id).Error; err != nil {
return nil, err
}
return &policy, nil
}
// CreatePolicyRequest represents the request body for creating an auth policy
type CreatePolicyRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
AllowedRoles string `json:"allowed_roles"`
AllowedUsers string `json:"allowed_users"`
AllowedDomains string `json:"allowed_domains"`
RequireMFA bool `json:"require_mfa"`
SessionTimeout int `json:"session_timeout"`
}
// Create creates a new auth policy
func (h *AuthPolicyHandler) Create(c *gin.Context) {
var req CreatePolicyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
policy := models.AuthPolicy{
Name: req.Name,
Description: req.Description,
AllowedRoles: req.AllowedRoles,
AllowedUsers: req.AllowedUsers,
AllowedDomains: req.AllowedDomains,
RequireMFA: req.RequireMFA,
SessionTimeout: req.SessionTimeout,
Enabled: true,
}
if err := h.db.Create(&policy).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, policy)
}
// UpdatePolicyRequest represents the request body for updating an auth policy
type UpdatePolicyRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
AllowedRoles *string `json:"allowed_roles,omitempty"`
AllowedUsers *string `json:"allowed_users,omitempty"`
AllowedDomains *string `json:"allowed_domains,omitempty"`
RequireMFA *bool `json:"require_mfa,omitempty"`
SessionTimeout *int `json:"session_timeout,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
// Update updates an existing auth policy
func (h *AuthPolicyHandler) Update(c *gin.Context) {
uuid := c.Param("uuid")
var policy models.AuthPolicy
if err := h.db.Where("uuid = ?", uuid).First(&policy).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var req UpdatePolicyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Name != nil {
policy.Name = *req.Name
}
if req.Description != nil {
policy.Description = *req.Description
}
if req.AllowedRoles != nil {
policy.AllowedRoles = *req.AllowedRoles
}
if req.AllowedUsers != nil {
policy.AllowedUsers = *req.AllowedUsers
}
if req.AllowedDomains != nil {
policy.AllowedDomains = *req.AllowedDomains
}
if req.RequireMFA != nil {
policy.RequireMFA = *req.RequireMFA
}
if req.SessionTimeout != nil {
policy.SessionTimeout = *req.SessionTimeout
}
if req.Enabled != nil {
policy.Enabled = *req.Enabled
}
if err := h.db.Save(&policy).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, policy)
}
// Delete deletes an auth policy
func (h *AuthPolicyHandler) Delete(c *gin.Context) {
uuid := c.Param("uuid")
// Get the policy first to get its ID
var policy models.AuthPolicy
if err := h.db.Where("uuid = ?", uuid).First(&policy).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Check if any proxy hosts are using this policy
var count int64
h.db.Model(&models.ProxyHost{}).Where("auth_policy_id = ?", policy.ID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete policy that is in use by proxy hosts"})
return
}
if err := h.db.Delete(&policy).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Policy deleted successfully"})
}
@@ -1,645 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupAuthHandlersTestDB(t *testing.T) *gorm.DB {
dsn := filepath.Join(t.TempDir(), "test.db") + "?_busy_timeout=5000&_journal_mode=WAL"
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
err = db.AutoMigrate(
&models.AuthUser{},
&models.AuthProvider{},
&models.AuthPolicy{},
&models.ProxyHost{},
)
require.NoError(t, err)
return db
}
func setupAuthTestRouter(db *gorm.DB) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
userHandler := NewAuthUserHandler(db)
providerHandler := NewAuthProviderHandler(db)
policyHandler := NewAuthPolicyHandler(db)
api := router.Group("/api/v1")
// Auth User routes
api.GET("/security/users", userHandler.List)
api.GET("/security/users/stats", userHandler.Stats)
api.GET("/security/users/:uuid", userHandler.Get)
api.POST("/security/users", userHandler.Create)
api.PUT("/security/users/:uuid", userHandler.Update)
api.DELETE("/security/users/:uuid", userHandler.Delete)
// Auth Provider routes
api.GET("/security/providers", providerHandler.List)
api.GET("/security/providers/:uuid", providerHandler.Get)
api.POST("/security/providers", providerHandler.Create)
api.PUT("/security/providers/:uuid", providerHandler.Update)
api.DELETE("/security/providers/:uuid", providerHandler.Delete)
// Auth Policy routes
api.GET("/security/policies", policyHandler.List)
api.GET("/security/policies/:uuid", policyHandler.Get)
api.POST("/security/policies", policyHandler.Create)
api.PUT("/security/policies/:uuid", policyHandler.Update)
api.DELETE("/security/policies/:uuid", policyHandler.Delete)
return router
}
// ==================== Auth User Tests ====================
func TestAuthUserHandler_List(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
// Create test users
user := models.AuthUser{Username: "testuser", Email: "test@example.com", Enabled: true}
user.SetPassword("password123")
db.Create(&user)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/users", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var users []models.AuthUser
err := json.Unmarshal(w.Body.Bytes(), &users)
assert.NoError(t, err)
assert.Len(t, users, 1)
assert.Equal(t, "testuser", users[0].Username)
}
func TestAuthUserHandler_Get(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
user := models.AuthUser{Username: "testuser", Email: "test@example.com"}
user.SetPassword("password123")
db.Create(&user)
t.Run("found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/users/"+user.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var result models.AuthUser
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "testuser", result.Username)
})
t.Run("not found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/users/nonexistent", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthUserHandler_Create(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
t.Run("success", func(t *testing.T) {
body := map[string]interface{}{
"username": "newuser",
"email": "new@example.com",
"name": "New User",
"password": "password123",
"roles": "user",
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/security/users", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var result models.AuthUser
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "newuser", result.Username)
assert.True(t, result.Enabled)
})
t.Run("with additional emails", func(t *testing.T) {
body := map[string]interface{}{
"username": "multiemail",
"email": "primary@example.com",
"password": "password123",
"additional_emails": "alt1@example.com,alt2@example.com",
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/security/users", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var result models.AuthUser
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "multiemail", result.Username)
assert.Equal(t, "alt1@example.com,alt2@example.com", result.AdditionalEmails)
})
t.Run("invalid email", func(t *testing.T) {
body := map[string]interface{}{
"username": "baduser",
"email": "not-an-email",
"password": "password123",
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/security/users", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
func TestAuthUserHandler_Update(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
user := models.AuthUser{Username: "testuser", Email: "test@example.com", Enabled: true}
user.SetPassword("password123")
db.Create(&user)
t.Run("success", func(t *testing.T) {
body := map[string]interface{}{
"name": "Updated Name",
"enabled": false,
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/security/users/"+user.UUID, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var result models.AuthUser
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "Updated Name", result.Name)
assert.False(t, result.Enabled)
})
t.Run("update additional emails", func(t *testing.T) {
body := map[string]interface{}{
"additional_emails": "newalt@example.com",
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/security/users/"+user.UUID, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var result models.AuthUser
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "newalt@example.com", result.AdditionalEmails)
})
t.Run("not found", func(t *testing.T) {
body := map[string]interface{}{"name": "Test"}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/security/users/nonexistent", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthUserHandler_Delete(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
t.Run("success", func(t *testing.T) {
user := models.AuthUser{Username: "deleteuser", Email: "delete@example.com"}
user.SetPassword("password123")
db.Create(&user)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/security/users/"+user.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify deleted
var count int64
db.Model(&models.AuthUser{}).Where("uuid = ?", user.UUID).Count(&count)
assert.Equal(t, int64(0), count)
})
t.Run("cannot delete last admin", func(t *testing.T) {
admin := models.AuthUser{Username: "admin", Email: "admin@example.com", Roles: "admin"}
admin.SetPassword("password123")
db.Create(&admin)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/security/users/"+admin.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "last admin")
})
t.Run("not found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/security/users/nonexistent", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthUserHandler_Stats(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
user1 := models.AuthUser{Username: "stats_user1", Email: "stats1@example.com", Enabled: true, MFAEnabled: true}
user1.SetPassword("password123")
// user2 needs Enabled: false, but GORM's default:true overrides the zero value
// So we create it first then update
user2 := models.AuthUser{Username: "stats_user2", Email: "stats2@example.com", MFAEnabled: false}
user2.SetPassword("password123")
require.NoError(t, db.Create(&user1).Error)
require.NoError(t, db.Create(&user2).Error)
// Explicitly set Enabled to false after create
require.NoError(t, db.Model(&user2).Update("enabled", false).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/users/stats", nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var stats map[string]int64
err := json.Unmarshal(w.Body.Bytes(), &stats)
require.NoError(t, err)
assert.Equal(t, int64(2), stats["total"])
assert.Equal(t, int64(1), stats["enabled"])
assert.Equal(t, int64(1), stats["with_mfa"])
}
// ==================== Auth Provider Tests ====================
func TestAuthProviderHandler_List(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
provider := models.AuthProvider{Name: "TestProvider", Type: "oidc", ClientID: "id", ClientSecret: "secret"}
db.Create(&provider)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/providers", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var providers []models.AuthProvider
json.Unmarshal(w.Body.Bytes(), &providers)
assert.Len(t, providers, 1)
}
func TestAuthProviderHandler_Get(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
provider := models.AuthProvider{Name: "TestProvider", Type: "oidc", ClientID: "id", ClientSecret: "secret"}
db.Create(&provider)
t.Run("found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/providers/"+provider.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
})
t.Run("not found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/providers/nonexistent", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthProviderHandler_Create(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
t.Run("success", func(t *testing.T) {
body := map[string]interface{}{
"name": "NewProvider",
"type": "oidc",
"client_id": "client123",
"client_secret": "secret456",
"issuer_url": "https://auth.example.com",
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/security/providers", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
})
t.Run("missing required fields", func(t *testing.T) {
body := map[string]interface{}{
"name": "Incomplete",
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/security/providers", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
func TestAuthProviderHandler_Update(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
provider := models.AuthProvider{Name: "TestProvider", Type: "oidc", ClientID: "id", ClientSecret: "secret", Enabled: true}
db.Create(&provider)
t.Run("success", func(t *testing.T) {
body := map[string]interface{}{
"name": "UpdatedProvider",
"enabled": false,
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/security/providers/"+provider.UUID, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var result models.AuthProvider
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "UpdatedProvider", result.Name)
assert.False(t, result.Enabled)
})
t.Run("not found", func(t *testing.T) {
body := map[string]interface{}{"name": "Test"}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/security/providers/nonexistent", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthProviderHandler_Delete(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
provider := models.AuthProvider{Name: "DeleteProvider", Type: "oidc", ClientID: "id", ClientSecret: "secret"}
db.Create(&provider)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/security/providers/"+provider.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// ==================== Auth Policy Tests ====================
func TestAuthPolicyHandler_List(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
policy := models.AuthPolicy{Name: "TestPolicy", AllowedRoles: "admin"}
db.Create(&policy)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/policies", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var policies []models.AuthPolicy
json.Unmarshal(w.Body.Bytes(), &policies)
assert.Len(t, policies, 1)
}
func TestAuthPolicyHandler_Get(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
policy := models.AuthPolicy{Name: "TestPolicy"}
db.Create(&policy)
t.Run("found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/policies/"+policy.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
})
t.Run("not found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/security/policies/nonexistent", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthPolicyHandler_Create(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
t.Run("success", func(t *testing.T) {
body := map[string]interface{}{
"name": "NewPolicy",
"description": "A test policy",
"allowed_roles": "admin,user",
"session_timeout": 3600,
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/security/policies", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var result models.AuthPolicy
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "NewPolicy", result.Name)
assert.True(t, result.Enabled)
})
t.Run("missing required fields", func(t *testing.T) {
body := map[string]interface{}{
"description": "No name",
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/security/policies", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
func TestAuthPolicyHandler_Update(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
policy := models.AuthPolicy{Name: "TestPolicy", Enabled: true}
db.Create(&policy)
t.Run("success", func(t *testing.T) {
body := map[string]interface{}{
"name": "UpdatedPolicy",
"require_mfa": true,
"enabled": false,
}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/security/policies/"+policy.UUID, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var result models.AuthPolicy
json.Unmarshal(w.Body.Bytes(), &result)
assert.Equal(t, "UpdatedPolicy", result.Name)
assert.True(t, result.RequireMFA)
assert.False(t, result.Enabled)
})
t.Run("not found", func(t *testing.T) {
body := map[string]interface{}{"name": "Test"}
jsonBody, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/security/policies/nonexistent", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthPolicyHandler_Delete(t *testing.T) {
db := setupAuthHandlersTestDB(t)
router := setupAuthTestRouter(db)
t.Run("success", func(t *testing.T) {
policy := models.AuthPolicy{Name: "DeletePolicy"}
db.Create(&policy)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/security/policies/"+policy.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
})
t.Run("cannot delete policy in use", func(t *testing.T) {
policy := models.AuthPolicy{Name: "InUsePolicy"}
db.Create(&policy)
// Create a proxy host using this policy
host := models.ProxyHost{
UUID: "test-host-uuid",
DomainNames: "test.com",
ForwardHost: "localhost",
ForwardPort: 80,
AuthPolicyID: &policy.ID,
}
db.Create(&host)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/security/policies/"+policy.UUID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "in use")
})
t.Run("not found", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/security/policies/nonexistent", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
func TestAuthPolicyHandler_GetByID(t *testing.T) {
db := setupAuthHandlersTestDB(t)
handler := NewAuthPolicyHandler(db)
policy := models.AuthPolicy{Name: "TestPolicy"}
db.Create(&policy)
t.Run("found", func(t *testing.T) {
result, err := handler.GetByID(policy.ID)
assert.NoError(t, err)
assert.Equal(t, "TestPolicy", result.Name)
})
t.Run("not found", func(t *testing.T) {
_, err := handler.GetByID(9999)
assert.Error(t, err)
})
}
@@ -1,109 +0,0 @@
package handlers
import (
"net/http"
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ForwardAuthHandler handles forward authentication configuration endpoints.
type ForwardAuthHandler struct {
db *gorm.DB
}
// NewForwardAuthHandler creates a new handler.
func NewForwardAuthHandler(db *gorm.DB) *ForwardAuthHandler {
return &ForwardAuthHandler{db: db}
}
// GetConfig retrieves the forward auth configuration.
func (h *ForwardAuthHandler) GetConfig(c *gin.Context) {
var config models.ForwardAuthConfig
if err := h.db.First(&config).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// Return default/empty config
c.JSON(http.StatusOK, models.ForwardAuthConfig{
Provider: "custom",
Address: "",
TrustForwardHeader: true,
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch config"})
return
}
c.JSON(http.StatusOK, config)
}
// UpdateConfig updates or creates the forward auth configuration.
func (h *ForwardAuthHandler) UpdateConfig(c *gin.Context) {
var input struct {
Provider string `json:"provider" binding:"required,oneof=authelia authentik pomerium custom"`
Address string `json:"address" binding:"required,url"`
TrustForwardHeader bool `json:"trust_forward_header"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var config models.ForwardAuthConfig
err := h.db.First(&config).Error
if err == gorm.ErrRecordNotFound {
// Create new config
config = models.ForwardAuthConfig{
Provider: input.Provider,
Address: input.Address,
TrustForwardHeader: input.TrustForwardHeader,
}
if err := h.db.Create(&config).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create config"})
return
}
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch config"})
return
} else {
// Update existing config
config.Provider = input.Provider
config.Address = input.Address
config.TrustForwardHeader = input.TrustForwardHeader
if err := h.db.Save(&config).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update config"})
return
}
}
c.JSON(http.StatusOK, config)
}
// GetTemplates returns pre-configured templates for popular providers.
func (h *ForwardAuthHandler) GetTemplates(c *gin.Context) {
templates := map[string]interface{}{
"authelia": gin.H{
"provider": "authelia",
"address": "http://authelia:9091/api/verify",
"trust_forward_header": true,
"description": "Authelia authentication server",
},
"authentik": gin.H{
"provider": "authentik",
"address": "http://authentik-server:9000/outpost.goauthentik.io/auth/caddy",
"trust_forward_header": true,
"description": "Authentik SSO provider",
},
"pomerium": gin.H{
"provider": "pomerium",
"address": "https://verify.pomerium.app",
"trust_forward_header": true,
"description": "Pomerium identity-aware proxy",
},
}
c.JSON(http.StatusOK, templates)
}
@@ -1,118 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupForwardAuthTestDB() *gorm.DB {
db, _ := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
db.AutoMigrate(&models.ForwardAuthConfig{})
return db
}
func TestForwardAuthHandler_GetConfig(t *testing.T) {
db := setupForwardAuthTestDB()
h := NewForwardAuthHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/config", h.GetConfig)
// Test empty config (default)
req, _ := http.NewRequest("GET", "/config", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp models.ForwardAuthConfig
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, "custom", resp.Provider)
// Test existing config
db.Create(&models.ForwardAuthConfig{
Provider: "authelia",
Address: "http://test",
})
req, _ = http.NewRequest("GET", "/config", nil)
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, "authelia", resp.Provider)
}
func TestForwardAuthHandler_UpdateConfig(t *testing.T) {
db := setupForwardAuthTestDB()
h := NewForwardAuthHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.POST("/config", h.UpdateConfig)
// Test Create
payload := map[string]interface{}{
"provider": "authelia",
"address": "http://authelia:9091",
"trust_forward_header": true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/config", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp models.ForwardAuthConfig
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, "authelia", resp.Provider)
// Test Update
payload["provider"] = "authentik"
body, _ = json.Marshal(payload)
req, _ = http.NewRequest("POST", "/config", bytes.NewBuffer(body))
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, "authentik", resp.Provider)
// Test Validation Error
payload["address"] = "not-a-url"
body, _ = json.Marshal(payload)
req, _ = http.NewRequest("POST", "/config", bytes.NewBuffer(body))
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestForwardAuthHandler_GetTemplates(t *testing.T) {
db := setupForwardAuthTestDB()
h := NewForwardAuthHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/templates", h.GetTemplates)
req, _ := http.NewRequest("GET", "/templates", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Contains(t, resp, "authelia")
assert.Contains(t, resp, "authentik")
}
-33
View File
@@ -34,10 +34,6 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error {
&models.UptimeMonitor{},
&models.UptimeHeartbeat{},
&models.Domain{},
&models.ForwardAuthConfig{},
&models.AuthUser{},
&models.AuthProvider{},
&models.AuthPolicy{},
); err != nil {
return fmt.Errorf("auto migrate: %w", err)
}
@@ -104,12 +100,6 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error {
protected.GET("/settings", settingsHandler.GetSettings)
protected.POST("/settings", settingsHandler.UpdateSetting)
// Forward Auth
forwardAuthHandler := handlers.NewForwardAuthHandler(db)
protected.GET("/security/forward-auth", forwardAuthHandler.GetConfig)
protected.PUT("/security/forward-auth", forwardAuthHandler.UpdateConfig)
protected.GET("/security/forward-auth/templates", forwardAuthHandler.GetTemplates)
// User Profile & API Key
userHandler := handlers.NewUserHandler(db)
protected.GET("/user/profile", userHandler.GetProfile)
@@ -203,29 +193,6 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error {
api.POST("/certificates", certHandler.Upload)
api.DELETE("/certificates/:id", certHandler.Delete)
// Security endpoints (Built-in SSO)
authUserHandler := handlers.NewAuthUserHandler(db)
api.GET("/security/users", authUserHandler.List)
api.GET("/security/users/stats", authUserHandler.Stats)
api.GET("/security/users/:uuid", authUserHandler.Get)
api.POST("/security/users", authUserHandler.Create)
api.PUT("/security/users/:uuid", authUserHandler.Update)
api.DELETE("/security/users/:uuid", authUserHandler.Delete)
authProviderHandler := handlers.NewAuthProviderHandler(db)
api.GET("/security/providers", authProviderHandler.List)
api.GET("/security/providers/:uuid", authProviderHandler.Get)
api.POST("/security/providers", authProviderHandler.Create)
api.PUT("/security/providers/:uuid", authProviderHandler.Update)
api.DELETE("/security/providers/:uuid", authProviderHandler.Delete)
authPolicyHandler := handlers.NewAuthPolicyHandler(db)
api.GET("/security/policies", authPolicyHandler.List)
api.GET("/security/policies/:uuid", authPolicyHandler.Get)
api.POST("/security/policies", authPolicyHandler.Create)
api.PUT("/security/policies/:uuid", authPolicyHandler.Update)
api.DELETE("/security/policies/:uuid", authPolicyHandler.Delete)
// Initial Caddy Config Sync
go func() {
// Wait for Caddy to be ready (max 30 seconds)