Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,580 @@
|
||||
package v1
|
||||
|
||||
// Reference: M13 §4.4 — LDAP HTTP endpoints for login, config, and connectivity testing
|
||||
// Provides four endpoints for LDAP/Active Directory integration:
|
||||
// - POST /api/v1/ldap/login → LDAP bind authentication + JWT issuance
|
||||
// - POST /api/v1/ldap/test → Admin-only LDAP connectivity test
|
||||
// - GET /api/v1/ldap/config → Admin-only LDAP settings retrieval
|
||||
// - PUT /api/v1/ldap/config → Admin-only LDAP settings update
|
||||
//
|
||||
// Enterprise feature: GoChat extends beyond Chatwoot's SAML-only SSO by adding
|
||||
// LDAP support for traditional enterprise AD/LDAP environments.
|
||||
//
|
||||
// Login flow:
|
||||
// 1. Client sends {username, password, account_id} to /api/v1/ldap/login
|
||||
// 2. Handler delegates to SSOMiddleware.AuthenticateLDAP for unified SSO processing
|
||||
// 3. SSOMiddleware routes to LDAPService.Authenticate (Bind + search + group extraction)
|
||||
// 4. On success: SSOMiddleware auto-provisions user, maps groups→roles, creates SSO session
|
||||
// 5. Handler issues JWT token pair via JWTService.GenerateTokenPair
|
||||
// 6. Store refresh token, return access+refresh tokens to client
|
||||
//
|
||||
// Config management (admin-only):
|
||||
// - Account administrators can configure per-account LDAP settings
|
||||
// - TestConnection validates LDAP bind connectivity before saving config
|
||||
// - GetConfig/UpdateConfig manage per-account LDAP settings in DB
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/config"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// LDAPHandler handles LDAP authentication HTTP endpoints.
|
||||
type LDAPHandler struct {
|
||||
ldapService *auth.LDAPService
|
||||
ssoMiddleware *auth.SSOMiddleware
|
||||
jwtService *auth.JWTService
|
||||
refreshStore *auth.RefreshTokenStore
|
||||
ssoSessionStore *auth.SSOSessionStore
|
||||
ldapCfg *config.LDAPConfig
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewLDAPHandler creates an LDAP handler with service dependencies.
|
||||
func NewLDAPHandler(
|
||||
ldapService *auth.LDAPService,
|
||||
ssoMiddleware *auth.SSOMiddleware,
|
||||
jwtService *auth.JWTService,
|
||||
refreshStore *auth.RefreshTokenStore,
|
||||
ssoSessionStore *auth.SSOSessionStore,
|
||||
ldapCfg *config.LDAPConfig,
|
||||
db *gorm.DB,
|
||||
) *LDAPHandler {
|
||||
return &LDAPHandler{
|
||||
ldapService: ldapService,
|
||||
ssoMiddleware: ssoMiddleware,
|
||||
jwtService: jwtService,
|
||||
refreshStore: refreshStore,
|
||||
ssoSessionStore: ssoSessionStore,
|
||||
ldapCfg: ldapCfg,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ldapLoginRequest is the JSON body for POST /api/v1/ldap/login.
|
||||
type ldapLoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
AccountID uint `json:"account_id" binding:"required"`
|
||||
}
|
||||
|
||||
// Login authenticates a user via LDAP bind and issues a JWT token pair.
|
||||
// POST /api/v1/ldap/login
|
||||
// This endpoint is PUBLIC — no AuthMiddleware required (LDAP login is the entry point).
|
||||
func (h *LDAPHandler) Login(c *gin.Context) {
|
||||
if !h.ldapCfg.Enabled {
|
||||
c.JSON(http.StatusNotFound, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrNotFound,
|
||||
Message: "LDAP is not enabled",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req ldapLoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrBadRequest,
|
||||
Message: "Invalid request body",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Delegate to SSO middleware for unified authentication flow
|
||||
// (auto-provision, group→role mapping, SSO session creation)
|
||||
result, err := h.ssoMiddleware.AuthenticateLDAP(c.Request.Context(), req.AccountID, req.Username, req.Password)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("LDAP authentication failed (account=%d, username=%s): %v", req.AccountID, req.Username, err)
|
||||
|
||||
statusCode := http.StatusInternalServerError
|
||||
errCode := response.ErrInternal
|
||||
if errors.Is(err, auth.ErrLDAPDisabled) {
|
||||
statusCode = http.StatusNotFound
|
||||
errCode = response.ErrNotFound
|
||||
} else if errors.Is(err, auth.ErrLDAPInvalidConfig) {
|
||||
statusCode = http.StatusBadRequest
|
||||
errCode = response.ErrBadRequest
|
||||
} else if errors.Is(err, auth.ErrLDAPConnection) {
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
errCode = response.ErrServiceUnavail
|
||||
} else if errors.Is(err, auth.ErrLDAPUserNotFound) || errors.Is(err, auth.ErrLDAPBindFailed) {
|
||||
statusCode = http.StatusUnauthorized
|
||||
errCode = response.ErrUnauthorized
|
||||
}
|
||||
|
||||
c.JSON(statusCode, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: errCode,
|
||||
Message: "LDAP authentication failed",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Issue JWT token pair using JWTService
|
||||
// SSO middleware already created the user and mapped roles
|
||||
tokenPair, err := h.jwtService.GenerateTokenPair(
|
||||
&model.User{
|
||||
Base: model.Base{ID: result.UserID},
|
||||
Email: result.Email,
|
||||
Name: result.Name,
|
||||
Provider: "ldap",
|
||||
UID: result.Subject,
|
||||
Role: result.Role,
|
||||
},
|
||||
result.AccountID,
|
||||
result.Role,
|
||||
)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to generate JWT for LDAP user (account=%d, user=%d): %v", result.AccountID, result.UserID, err)
|
||||
c.JSON(http.StatusUnprocessableEntity, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrInternal,
|
||||
Message: "Failed to generate authentication tokens",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Store refresh token
|
||||
if h.refreshStore != nil {
|
||||
if err := h.refreshStore.Store(c.Request.Context(), result.UserID, tokenPair.RefreshToken); err != nil {
|
||||
applogger.L().Warnf("Failed to store refresh token for LDAP user %d: %v", result.UserID, err)
|
||||
// Non-fatal: access token is still valid, refresh just won't work until re-login
|
||||
}
|
||||
}
|
||||
|
||||
// Create SSO session in Redis (for session tracking and SLO)
|
||||
if h.ssoSessionStore != nil {
|
||||
sessionData := &auth.SSOSessionData{
|
||||
UserID: result.UserID,
|
||||
Provider: "ldap",
|
||||
IdPEntityID: fmt.Sprintf("ldap-account-%d", result.AccountID), // LDAP server as IdP identifier
|
||||
NameID: result.Subject, // LDAP DN as NameID
|
||||
AccountID: result.AccountID,
|
||||
Role: result.Role,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
ExpiresAt: time.Now().Add(h.ssoSessionStore.SessionTTL()).Unix(),
|
||||
}
|
||||
sessionID, err := h.ssoSessionStore.Create(c.Request.Context(), sessionData)
|
||||
if err != nil {
|
||||
applogger.L().Warnf("Failed to create SSO session for LDAP user %d: %v", result.UserID, err)
|
||||
// Non-fatal: JWT tokens are still valid, SSO session is for tracking/SLO only
|
||||
} else {
|
||||
applogger.L().Infof("SSO session %s created for LDAP user %d (account=%d)", sessionID, result.UserID, result.AccountID)
|
||||
}
|
||||
}
|
||||
|
||||
// Return successful auth response (same format as SAML ACS and regular login)
|
||||
response.OK(c, gin.H{
|
||||
"user": gin.H{
|
||||
"id": result.UserID,
|
||||
"email": result.Email,
|
||||
"name": result.Name,
|
||||
"provider": "ldap",
|
||||
"uid": result.Subject,
|
||||
"role": result.Role,
|
||||
},
|
||||
"access_token": tokenPair.AccessToken,
|
||||
"refresh_token": tokenPair.RefreshToken,
|
||||
"expires_at": tokenPair.ExpiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
// ldapTestRequest is the JSON body for POST /api/v1/ldap/test.
|
||||
type ldapTestRequest struct {
|
||||
AccountID uint `json:"account_id" binding:"required"`
|
||||
}
|
||||
|
||||
// getAccountSettingsFromDB loads per-account LDAP configuration from DB directly.
|
||||
// This is needed because LDAPService.getAccountSettings is unexported.
|
||||
func (h *LDAPHandler) getAccountSettingsFromDB(accountID uint) (*model.AccountLDAPSettings, error) {
|
||||
var settings model.AccountLDAPSettings
|
||||
err := h.db.Where("account_id = ? AND active = ?", accountID, true).First(&settings).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// No per-account settings — check if global defaults exist
|
||||
if h.ldapCfg.DefaultHost == "" {
|
||||
return nil, nil // LDAP not configured for this account
|
||||
}
|
||||
// Use global defaults as a fallback
|
||||
settings = model.AccountLDAPSettings{
|
||||
AccountID: accountID,
|
||||
Host: h.ldapCfg.DefaultHost,
|
||||
Port: h.ldapCfg.DefaultPort,
|
||||
UseTLS: h.ldapCfg.DefaultUseTLS,
|
||||
BaseDN: h.ldapCfg.DefaultBaseDN,
|
||||
BindDN: h.ldapCfg.DefaultBindDN,
|
||||
BindPassword: h.ldapCfg.DefaultBindPassword,
|
||||
UserFilter: h.ldapCfg.DefaultUserFilter,
|
||||
EmailAttribute: h.ldapCfg.DefaultEmailAttribute,
|
||||
NameAttribute: h.ldapCfg.DefaultNameAttribute,
|
||||
GroupAttribute: h.ldapCfg.DefaultGroupAttribute,
|
||||
AutoProvision: true,
|
||||
Active: true,
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// testLDAPConnectivity tests LDAP bind connectivity for an account's configuration.
|
||||
// Connects to the LDAP server and attempts a bind with the service account to verify
|
||||
// that the configuration is correct before saving.
|
||||
func (h *LDAPHandler) testLDAPConnectivity(settings *model.AccountLDAPSettings) error {
|
||||
// Use LDAPService.Authenticate with a dummy test to verify connectivity.
|
||||
// The LDAPService handles connection + bind internally, so we use it to validate.
|
||||
// We attempt a bind-only test by calling Authenticate with empty credentials
|
||||
// and catching the specific error pattern.
|
||||
// However, since Authenticate requires a real username/password, we instead
|
||||
// try to directly connect and bind using the service account credentials.
|
||||
//
|
||||
// For simplicity, we delegate to ldapService.Authenticate with a test username.
|
||||
// If the connection itself fails, we get ErrLDAPConnection.
|
||||
// If the service account bind fails, we get an appropriate error.
|
||||
// If the user search fails (expected for test username), we know connectivity works.
|
||||
ctx := context.Background()
|
||||
_, err := h.ldapService.Authenticate(ctx, settings.AccountID, "__ldap_connectivity_test__", "__invalid_test_password__")
|
||||
if err == nil {
|
||||
// Unexpected: test credentials actually worked. Still means connectivity is good.
|
||||
return nil
|
||||
}
|
||||
// If connection failed, return that error
|
||||
if errors.Is(err, auth.ErrLDAPConnection) || errors.Is(err, auth.ErrLDAPDisabled) || errors.Is(err, auth.ErrLDAPInvalidConfig) {
|
||||
return err
|
||||
}
|
||||
// If we got ErrLDAPUserNotFound or ErrLDAPBindFailed, that means the connection
|
||||
// and service account bind succeeded — only the test user lookup/bind failed,
|
||||
// which is expected. Connectivity is confirmed.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestConnection tests LDAP bind connectivity for an account's configuration.
|
||||
// POST /api/v1/ldap/test
|
||||
// Admin-only: requires AuthMiddleware + admin role (enforced at router level).
|
||||
func (h *LDAPHandler) TestConnection(c *gin.Context) {
|
||||
if !h.ldapCfg.Enabled {
|
||||
c.JSON(http.StatusNotFound, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrNotFound,
|
||||
Message: "LDAP is not enabled",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req ldapTestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrBadRequest,
|
||||
Message: "Invalid request body",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Load per-account LDAP settings from DB
|
||||
settings, err := h.getAccountSettingsFromDB(req.AccountID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to load LDAP settings for account %d: %v", req.AccountID, err)
|
||||
c.JSON(http.StatusUnprocessableEntity, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrInternal,
|
||||
Message: "Failed to load LDAP settings",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if settings == nil {
|
||||
c.JSON(http.StatusNotFound, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrNotFound,
|
||||
Message: "No LDAP configuration found for this account",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Test LDAP connectivity
|
||||
err = h.testLDAPConnectivity(settings)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("LDAP connectivity test failed (account=%d, host=%s:%d): %v", req.AccountID, settings.Host, settings.Port, err)
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrBadRequest,
|
||||
Message: "LDAP connectivity test failed",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{
|
||||
"account_id": req.AccountID,
|
||||
"host": settings.Host,
|
||||
"port": settings.Port,
|
||||
"connected": true,
|
||||
})
|
||||
}
|
||||
|
||||
// GetConfig retrieves LDAP settings for an account.
|
||||
// GET /api/v1/ldap/config?account_id=123
|
||||
// Admin-only: requires AuthMiddleware + admin role (enforced at router level).
|
||||
func (h *LDAPHandler) GetConfig(c *gin.Context) {
|
||||
if !h.ldapCfg.Enabled {
|
||||
c.JSON(http.StatusNotFound, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrNotFound,
|
||||
Message: "LDAP is not enabled",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
accountIDStr := c.Query("account_id")
|
||||
if accountIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrBadRequest,
|
||||
Message: "account_id query parameter is required",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrBadRequest,
|
||||
Message: "Invalid account_id",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.getAccountSettingsFromDB(uint(accountID))
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get LDAP config for account %d: %v", accountID, err)
|
||||
c.JSON(http.StatusUnprocessableEntity, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrInternal,
|
||||
Message: "Failed to retrieve LDAP configuration",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if settings == nil {
|
||||
c.JSON(http.StatusNotFound, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrNotFound,
|
||||
Message: "No LDAP configuration found for this account",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, settings)
|
||||
}
|
||||
|
||||
// ldapUpdateConfigRequest is the JSON body for PUT /api/v1/ldap/config.
|
||||
type ldapUpdateConfigRequest struct {
|
||||
AccountID uint `json:"account_id" binding:"required"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
Port int `json:"port"`
|
||||
UseTLS bool `json:"use_tls"`
|
||||
BaseDN string `json:"base_dn" binding:"required"`
|
||||
BindDN string `json:"bind_dn,omitempty"`
|
||||
BindPassword string `json:"bind_password,omitempty"`
|
||||
UserFilter string `json:"user_filter"`
|
||||
EmailAttribute string `json:"email_attribute"`
|
||||
NameAttribute string `json:"name_attribute"`
|
||||
FirstNameAttribute string `json:"first_name_attribute"`
|
||||
LastNameAttribute string `json:"last_name_attribute"`
|
||||
GroupAttribute string `json:"group_attribute"`
|
||||
GroupFilter string `json:"group_filter"`
|
||||
RoleMappings json.RawMessage `json:"role_mappings"`
|
||||
AutoProvision bool `json:"auto_provision"`
|
||||
SyncInterval int `json:"sync_interval"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// UpdateConfig updates per-account LDAP settings.
|
||||
// PUT /api/v1/ldap/config
|
||||
// Admin-only: requires AuthMiddleware + admin role (enforced at router level).
|
||||
func (h *LDAPHandler) UpdateConfig(c *gin.Context) {
|
||||
if !h.ldapCfg.Enabled {
|
||||
c.JSON(http.StatusNotFound, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrNotFound,
|
||||
Message: "LDAP is not enabled",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req ldapUpdateConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrBadRequest,
|
||||
Message: "Invalid request body",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Default port values
|
||||
if req.Port == 0 {
|
||||
if req.UseTLS {
|
||||
req.Port = 636
|
||||
} else {
|
||||
req.Port = 389
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing settings or create new
|
||||
var settings model.AccountLDAPSettings
|
||||
err := h.db.Where("account_id = ?", req.AccountID).First(&settings).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
applogger.L().Errorf("Failed to check existing LDAP settings for account %d: %v", req.AccountID, err)
|
||||
c.JSON(http.StatusUnprocessableEntity, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrInternal,
|
||||
Message: "Failed to check existing LDAP configuration",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Create new settings
|
||||
settings = model.AccountLDAPSettings{
|
||||
AccountID: req.AccountID,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
UseTLS: req.UseTLS,
|
||||
BaseDN: req.BaseDN,
|
||||
BindDN: req.BindDN,
|
||||
BindPassword: req.BindPassword,
|
||||
UserFilter: req.UserFilter,
|
||||
EmailAttribute: req.EmailAttribute,
|
||||
NameAttribute: req.NameAttribute,
|
||||
FirstNameAttribute: req.FirstNameAttribute,
|
||||
LastNameAttribute: req.LastNameAttribute,
|
||||
GroupAttribute: req.GroupAttribute,
|
||||
GroupFilter: req.GroupFilter,
|
||||
RoleMappings: req.RoleMappings,
|
||||
AutoProvision: req.AutoProvision,
|
||||
SyncInterval: req.SyncInterval,
|
||||
Active: req.Active,
|
||||
}
|
||||
} else {
|
||||
// Update existing settings
|
||||
settings.Host = req.Host
|
||||
settings.Port = req.Port
|
||||
settings.UseTLS = req.UseTLS
|
||||
settings.BaseDN = req.BaseDN
|
||||
settings.BindDN = req.BindDN
|
||||
settings.BindPassword = req.BindPassword
|
||||
settings.UserFilter = req.UserFilter
|
||||
settings.EmailAttribute = req.EmailAttribute
|
||||
settings.NameAttribute = req.NameAttribute
|
||||
settings.FirstNameAttribute = req.FirstNameAttribute
|
||||
settings.LastNameAttribute = req.LastNameAttribute
|
||||
settings.GroupAttribute = req.GroupAttribute
|
||||
settings.GroupFilter = req.GroupFilter
|
||||
settings.RoleMappings = req.RoleMappings
|
||||
settings.AutoProvision = req.AutoProvision
|
||||
settings.SyncInterval = req.SyncInterval
|
||||
settings.Active = req.Active
|
||||
}
|
||||
|
||||
// Save to DB
|
||||
if err := h.db.Save(&settings).Error; err != nil {
|
||||
applogger.L().Errorf("Failed to save LDAP settings for account %d: %v", req.AccountID, err)
|
||||
c.JSON(http.StatusUnprocessableEntity, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
Code: response.ErrInternal,
|
||||
Message: "Failed to save LDAP configuration",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
applogger.L().Infof("LDAP settings updated for account %d (host=%s, port=%d, active=%v)", req.AccountID, req.Host, req.Port, req.Active)
|
||||
response.OK(c, settings)
|
||||
}
|
||||
|
||||
// RegisterLDAPRoutes sets up LDAP routes on a Gin router group.
|
||||
// Login route is PUBLIC — no AuthRequired middleware (LDAP login doesn't require existing JWT).
|
||||
// Config management routes require AuthMiddleware + admin role (enforced at router level).
|
||||
func RegisterLDAPRoutes(rg *gin.RouterGroup, handler *LDAPHandler) {
|
||||
ldapGroup := rg.Group("/ldap")
|
||||
{
|
||||
// Public route: LDAP login (no AuthRequired middleware)
|
||||
ldapGroup.POST("/login", handler.Login)
|
||||
|
||||
// Admin-only routes: config management + connectivity test
|
||||
// These are wired into the authenticated + admin router group externally,
|
||||
// so AuthMiddleware + admin role check is enforced at the router level.
|
||||
ldapGroup.POST("/test", handler.TestConnection)
|
||||
ldapGroup.GET("/config", handler.GetConfig)
|
||||
ldapGroup.PUT("/config", handler.UpdateConfig)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user