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:
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user