package api import ( "encoding/json" "fmt" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "unicode" "unicode/utf8" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/updates" internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth" "github.com/rs/zerolog/log" ) // detectServiceName detects the actual systemd service name being used func detectServiceName() string { // Try common service names services := []string{"pulse-backend", "pulse", "pulse.service", "pulse-backend.service"} for _, service := range services { cmd := exec.Command("systemctl", "status", service) if err := cmd.Run(); err == nil { // Service exists if strings.HasSuffix(service, ".service") { return strings.TrimSuffix(service, ".service") } return service } } // Default to pulse-backend if no service found return "pulse-backend" } // validateBcryptHash ensures the hash is complete (60 characters) func validateBcryptHash(hash string) error { if len(hash) != 60 { return fmt.Errorf("invalid bcrypt hash: expected 60 characters, got %d. Hash may be truncated", len(hash)) } if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") { return fmt.Errorf("invalid bcrypt hash: must start with $2a$, $2b$, or $2y$") } return nil } const maxLocalAuthUsernameRunes = 128 func validateLocalAuthUsername(username string) error { if username == "" { return fmt.Errorf("username is required") } if username != strings.TrimSpace(username) { return fmt.Errorf("username must not begin or end with whitespace") } if utf8.RuneCountInString(username) > maxLocalAuthUsernameRunes { return fmt.Errorf("username must be at most %d characters", maxLocalAuthUsernameRunes) } for _, r := range username { if unicode.IsLetter(r) || unicode.IsDigit(r) || strings.ContainsRune("._@+-", r) { continue } return fmt.Errorf("username may contain only letters, numbers, periods, underscores, hyphens, plus signs, and @") } return nil } func quoteAuthEnvValue(value string) (string, error) { if strings.ContainsAny(value, "'\r\n\x00") { return "", fmt.Errorf("auth environment value contains unsupported characters") } return "'" + value + "'", nil } func renderAuthEnvFile(generatedAt time.Time, username, hashedPassword string) ([]byte, error) { quotedUsername, err := quoteAuthEnvValue(username) if err != nil { return nil, err } quotedPassword, err := quoteAuthEnvValue(hashedPassword) if err != nil { return nil, err } return []byte(fmt.Sprintf(`# Auto-generated by Pulse Quick Security Setup # Generated on %s # IMPORTANT: Keep authentication values quoted. PULSE_AUTH_USER=%s PULSE_AUTH_PASS=%s PULSE_AUDIT_LOG=true `, generatedAt.Format(time.RFC3339), quotedUsername, quotedPassword)), nil } func quoteSystemdEnvironment(name, value string) (string, error) { if strings.ContainsAny(name, "=\r\n\x00") || strings.ContainsAny(value, "\r\n\x00") { return "", fmt.Errorf("systemd environment value contains unsupported characters") } return "Environment=" + strconv.Quote(name+"="+value), nil } func renderSystemdAuthOverride(generatedAt time.Time, username, hashedPassword string) ([]byte, error) { usernameLine, err := quoteSystemdEnvironment("PULSE_AUTH_USER", username) if err != nil { return nil, err } passwordLine, err := quoteSystemdEnvironment("PULSE_AUTH_PASS", hashedPassword) if err != nil { return nil, err } auditLine, err := quoteSystemdEnvironment("PULSE_AUDIT_LOG", "true") if err != nil { return nil, err } return []byte(fmt.Sprintf(`# Auto-generated by Pulse Quick Security Setup # Generated on %s [Service] %s %s %s `, generatedAt.Format(time.RFC3339), usernameLine, passwordLine, auditLine)), nil } // isRunningAsRoot checks if the process has root privileges func isRunningAsRoot() bool { return os.Geteuid() == 0 } func ensureAdminSession(cfg *config.Config, w http.ResponseWriter, req *http.Request) bool { // Session users must match configured admin identity for privileged operations. if cookie, err := readSessionCookie(req); err == nil && cookie.Value != "" && ValidateSession(cookie.Value) { sessionUser := strings.TrimSpace(GetSessionUsername(cookie.Value)) configuredAdmin := "" if cfg != nil { configuredAdmin = strings.TrimSpace(cfg.AuthUser) } if configuredAdmin != "" && strings.EqualFold(sessionUser, configuredAdmin) { return true } // Org-scoped tenant sessions preserve canonical org management // privileges for settings-bound routes. orgScoped := false if org := GetOrganization(req.Context()); org != nil { orgID := strings.TrimSpace(org.ID) if orgID != "" && orgID != "default" { orgScoped = true if org.CanUserIDManage(sessionUser) { return true } } } if !orgScoped && sessionUserCarriesAdminPrivileges(cfg, sessionUser) { return true } if configuredAdmin == "" || !strings.EqualFold(sessionUser, configuredAdmin) { log.Warn(). Str("path", req.URL.Path). Str("user", sessionUser). Msg("Session user missing admin privileges for privileged operation") http.Error(w, "Admin privileges required", http.StatusForbidden) return false } } return true } // sessionIsOrgScoped reports whether the request is bound to a tenant // organization rather than the instance itself. Instance-admin rules must not // be applied to an org-scoped session: those callers are governed by their // organization's own management rules rather than global RBAC assignments. func sessionIsOrgScoped(req *http.Request) bool { if req == nil { return false } org := GetOrganization(req.Context()) if org == nil { return false } orgID := strings.TrimSpace(org.ID) return orgID != "" && orgID != "default" } // sessionUserCarriesAdminPrivileges reports whether a non-org-scoped session // username carries instance admin privileges: the configured local admin // identity or an RBAC assignment granting the admin action on all resources. // SSO authentication alone never grants instance administration; SSO-only // deployments must map a trusted identity-provider group to an administrator // role or retain a configured local administrator. func sessionUserCarriesAdminPrivileges(cfg *config.Config, sessionUser string) bool { sessionUser = strings.TrimSpace(sessionUser) if sessionUser == "" { return false } configuredAdmin := "" if cfg != nil { configuredAdmin = strings.TrimSpace(cfg.AuthUser) } if configuredAdmin != "" && strings.EqualFold(sessionUser, configuredAdmin) { return true } if sessionUserHasRBACAdminGrant(sessionUser) { return true } return false } // sessionUserHasRBACAdminGrant reports whether the user's effective RBAC // permissions include an allow of the admin action on all resources — the // shape of the built-in Administrator role that SSO group role mappings // assign. A missing manager or empty assignment simply reports false. func sessionUserHasRBACAdminGrant(username string) bool { username = strings.TrimSpace(username) if username == "" { return false } manager := internalauth.GetManager() if manager == nil { return false } for _, perm := range manager.GetUserPermissions(username) { if strings.EqualFold(perm.Effect, internalauth.EffectDeny) { continue } if perm.Action == "admin" && perm.Resource == "*" { return true } } return false } func ensureSettingsScope(cfg *config.Config, w http.ResponseWriter, req *http.Request, scope string) bool { record := getAPITokenRecordFromRequest(req) if record != nil { if record.HasScope(scope) { return true } log.Warn(). Str("token_id", record.ID). Str("path", req.URL.Path). Str("required_scope", scope). Msg("API token missing required settings scope for privileged operation") respondMissingScope(w, scope) return false } if !ensureAdminSession(cfg, w, req) { return false } return true } func ensureSettingsReadScope(cfg *config.Config, w http.ResponseWriter, req *http.Request) bool { return ensureSettingsScope(cfg, w, req, config.ScopeSettingsRead) } func ensureSettingsWriteScope(cfg *config.Config, w http.ResponseWriter, req *http.Request) bool { return ensureSettingsScope(cfg, w, req, config.ScopeSettingsWrite) } // handleQuickSecuritySetupFixed is the fixed version of the Quick Security Setup func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Apply rate limiting to prevent brute force attacks clientIP := GetClientIP(req) if !authLimiter.Allow(clientIP) { log.Warn().Str("ip", clientIP).Msg("Rate limit exceeded for security setup") http.Error(w, "Too many attempts. Please try again later.", http.StatusTooManyRequests) return } // Parse request body var setupRequest struct { Username string `json:"username"` Password string `json:"password"` APIToken string `json:"apiToken"` EnableNotifications bool `json:"enableNotifications"` DarkMode bool `json:"darkMode"` Force bool `json:"force"` SetupToken string `json:"setupToken"` } if err := decodeSecurityRequestBody(w, req, &setupRequest); err != nil { http.Error(w, "Invalid request body", securityRequestErrorStatus(err)) return } authConfigured := r.config.AuthUser != "" && r.config.AuthPass != "" setupCompleted := false defer func() { if setupCompleted { r.clearBootstrapToken() } }() forceRequested := setupRequest.Force clientIP = GetClientIP(req) recoveryToken := strings.TrimSpace(req.Header.Get("X-Recovery-Token")) recoveryAuthorized := false if recoveryToken != "" { if GetRecoveryTokenStore().ValidateRecoveryTokenConstantTime(recoveryToken, clientIP) { recoveryAuthorized = true log.Warn(). Str("ip", clientIP). Msg("Quick security setup invoked using recovery token") } else { log.Warn(). Str("ip", clientIP). Msg("Invalid recovery token for quick security setup") } } authorized := recoveryAuthorized // Only require authentication if credentials are already configured. if !authorized && authConfigured { wrapped := &responseCapture{ResponseWriter: w} if checkAuth(r.config, wrapped, req, false) { // If proxy auth is configured, require admin role for changes. if r.config.ProxyAuthSecret != "" { if valid, username, isAdmin := CheckProxyAuth(r.config, req); valid && !isAdmin { log.Warn(). Str("ip", clientIP). Str("username", username). Msg("Non-admin user attempted quick security setup") http.Error(w, "Admin privileges required", http.StatusForbidden) return } } authorized = true } else { if !wrapped.wrote { http.Error(w, "Authentication required to modify existing security settings", http.StatusUnauthorized) } return } } if !authorized && !authConfigured { if r.bootstrapTokenHash == "" { log.Error().Msg("Bootstrap setup token unavailable; refusing unauthenticated quick setup") http.Error(w, "Bootstrap token unavailable; restart Pulse or inspect data directory", http.StatusServiceUnavailable) return } providedToken := strings.TrimSpace(req.Header.Get(bootstrapTokenHeader)) if providedToken == "" { providedToken = strings.TrimSpace(setupRequest.SetupToken) } // The bootstrap token is the security boundary for initial setup: // only callers with filesystem access to the data dir can read it. // A valid token authorizes setup from any origin so users running // Pulse in a Proxmox LXC (the common case) can complete setup from // their workstation browser. Without a token, only direct loopback // can finish setup — that lane preserves the legacy console flow. if providedToken == "" { if !isDirectLoopbackRequest(req) { log.Warn(). Str("ip", clientIP). Msg("Rejected initial quick setup: no bootstrap token from non-loopback origin") errorMsg := "Initial security setup requires the bootstrap token when accessed outside localhost. Retrieve it from the host:\n\n" + "Docker: docker exec /app/pulse bootstrap-token\n" + "Bare metal: pulse bootstrap-token" http.Error(w, errorMsg, http.StatusForbidden) return } errorMsg := "Bootstrap setup token required. Retrieve it from the host:\n\n" + "Docker: docker exec /app/pulse bootstrap-token\n" + "Bare metal: pulse bootstrap-token" http.Error(w, errorMsg, http.StatusUnauthorized) return } if !r.bootstrapTokenValid(providedToken) { log.Warn(). Str("ip", clientIP). Msg("Rejected quick setup with invalid bootstrap token") errorMsg := "Invalid bootstrap setup token. Retrieve the correct token from the host:\n\n" + "Docker: docker exec /app/pulse bootstrap-token\n" + "Bare metal: pulse bootstrap-token" http.Error(w, errorMsg, http.StatusUnauthorized) return } authorized = true } if authConfigured && !authorized { log.Warn(). Str("ip", clientIP). Msg("Unauthorized quick security setup attempt rejected") http.Error(w, "Authentication required to modify existing security settings", http.StatusUnauthorized) return } if authorized && !ensureSettingsWriteScope(r.config, w, req) { return } setupRequest.Force = forceRequested && authorized if authConfigured && !setupRequest.Force { log.Info().Msg("Security setup skipped - password auth already configured") response := map[string]interface{}{ "success": true, "skipped": true, "message": "Password authentication is already configured. Please remove existing security first if you want to reconfigure.", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } if setupRequest.Force { log.Info().Msg("Quick security setup invoked with force=true - rotating credentials") } // Validate inputs before any runtime or persistent security state changes. if setupRequest.Username == "" || setupRequest.Password == "" || setupRequest.APIToken == "" { http.Error(w, "Username, password, and API token are required", http.StatusBadRequest) return } if err := validateLocalAuthUsername(setupRequest.Username); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Validate password complexity if err := internalauth.ValidatePasswordComplexity(setupRequest.Password); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Hash the password hashedPassword, err := internalauth.HashPassword(setupRequest.Password) if err != nil { log.Error().Err(err).Msg("Failed to hash password") http.Error(w, "Failed to process password", http.StatusInternalServerError) return } // Validate the bcrypt hash is complete if err := validateBcryptHash(hashedPassword); err != nil { log.Error().Err(err).Msg("Generated invalid bcrypt hash") http.Error(w, "Failed to process password", http.StatusInternalServerError) return } authEnvContent, err := renderAuthEnvFile(time.Now(), setupRequest.Username, hashedPassword) if err != nil { log.Error().Err(err).Msg("Failed to encode authentication configuration") http.Error(w, "Failed to save security configuration", http.StatusInternalServerError) return } // Store the raw API token for displaying to the user rawAPIToken := setupRequest.APIToken tokenRecord, err := config.NewAPITokenRecord(rawAPIToken, "Primary token", nil) if err != nil { log.Error().Err(err).Msg("Failed to construct API token record") http.Error(w, "Failed to process API token", http.StatusInternalServerError) return } setAPITokenOwnerUserID(tokenRecord, setupRequest.Username) // Persist the canonical auth file for every deployment type before // updating runtime state. Systemd root installs also write an override // below, but the canonical file keeps config reloads and recovery // independent of service-name detection. envPath, err := writeAuthEnvFile(r.config.ConfigPath, r.config.DataPath, authEnvContent) if err != nil { log.Error().Err(err).Msg("Failed to write authentication configuration") http.Error(w, "Failed to save security configuration", http.StatusInternalServerError) return } if r.config.HasAPITokens() && r.config.AuthUser == "" && r.config.AuthPass == "" { // We had API-only access before, now replacing with full security log.Info().Msg("Replacing API-only token with new secure token") } // Update runtime config immediately with hashed token - no restart needed! config.Mu.Lock() previousTokens := append([]config.APITokenRecord(nil), r.config.APITokens...) r.config.AuthUser = setupRequest.Username r.config.AuthPass = hashedPassword r.config.APITokens = []config.APITokenRecord{*tokenRecord} r.config.SortAPITokens() if r.persistence != nil { if err := r.persistence.SaveAPITokens(r.config.APITokens); err != nil { r.config.APITokens = previousTokens r.config.SortAPITokens() config.Mu.Unlock() log.Error().Err(err).Msg("Failed to persist API tokens during security setup") http.Error(w, "Password authentication was saved, but the API token could not be saved", http.StatusInternalServerError) return } } config.Mu.Unlock() log.Info().Msg("Runtime config updated with new security settings - active immediately") if err := r.establishSession(w, req, setupRequest.Username); err != nil { log.Error().Err(err).Msg("Failed to establish session after quick security setup") http.Error(w, "Failed to establish session after setup", http.StatusInternalServerError) return } // Clear any agents that connected during the brief unauthenticated setup window. // This prevents stale/unauthorized agent data from appearing in the wizard. if r.monitor != nil { hostCleared, dockerCleared := r.monitor.ClearUnauthenticatedAgents() if hostCleared > 0 || dockerCleared > 0 { log.Info(). Int("hosts", hostCleared). Int("dockerHosts", dockerCleared). Msg("Cleared agents that connected before security was configured") } } // Save system settings to system.json systemSettings := config.DefaultSystemSettings() systemSettings.ConnectionTimeout = 10 // Default seconds systemSettings.AutoUpdateEnabled = false // Default disabled if err := r.persistence.SaveSystemSettings(*systemSettings); err != nil { log.Error().Err(err).Msg("Failed to save system settings") // Continue anyway - not critical for auth setup } // Detect environment isSystemd := os.Getenv("INVOCATION_ID") != "" isDocker := os.Getenv("PULSE_DOCKER") == "true" isRoot := isRunningAsRoot() // Detect actual service name if systemd serviceName := "" if isSystemd { serviceName = detectServiceName() log.Info().Str("service", serviceName).Msg("Detected systemd service name") } // Choose appropriate method based on environment if isDocker { log.Info().Str("path", envPath).Msg("Docker security configuration saved") response := map[string]interface{}{ "success": true, "method": "docker", "deploymentType": "docker", "requiresManualRestart": false, "message": "Security enabled immediately! Your settings are saved and active.", "note": "Configuration saved to /data/.env for persistence across restarts.", } w.Header().Set("Content-Type", "application/json") setupCompleted = true json.NewEncoder(w).Encode(response) } else if isSystemd && !isRoot { // Systemd but not root (ProxmoxVE script scenario) // Don't attempt sudo, just save config and provide instructions // Create response - security is active immediately response := map[string]interface{}{ "success": true, "method": "systemd-nonroot", "serviceName": serviceName, "envFile": envPath, "deploymentType": updates.GetDeploymentType(), "requiresManualRestart": false, "message": "Security enabled immediately! Your settings are saved and active.", "note": fmt.Sprintf("Configuration saved to %s for persistence across restarts.", envPath), } w.Header().Set("Content-Type", "application/json") setupCompleted = true json.NewEncoder(w).Encode(response) } else if isSystemd && isRoot { // Systemd with root - can apply directly // Create systemd override overridePath := fmt.Sprintf("/etc/systemd/system/%s.service.d/override.conf", serviceName) overrideDir := filepath.Dir(overridePath) if err := os.MkdirAll(overrideDir, 0755); err != nil { log.Error().Err(err).Msg("Failed to create override directory") http.Error(w, "Failed to create systemd override directory", http.StatusInternalServerError) return } overrideContent, err := renderSystemdAuthOverride(time.Now(), setupRequest.Username, hashedPassword) if err != nil { log.Error().Err(err).Msg("Failed to encode systemd authentication configuration") http.Error(w, "Failed to save security configuration", http.StatusInternalServerError) return } if err := os.WriteFile(overridePath, overrideContent, 0644); err != nil { log.Error().Err(err).Msg("Failed to write systemd override") http.Error(w, "Failed to write systemd override", http.StatusInternalServerError) return } // Reload systemd if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil { log.Warn().Err(err).Msg("Failed to reload systemd daemon") } response := map[string]interface{}{ "success": true, "method": "systemd-root", "serviceName": serviceName, "envFile": envPath, "deploymentType": updates.GetDeploymentType(), "automatic": true, "requiresManualRestart": false, "message": "Security enabled immediately! Your settings are saved and active.", "note": "Authentication file and systemd override created for persistence across restarts.", } w.Header().Set("Content-Type", "application/json") setupCompleted = true json.NewEncoder(w).Encode(response) } else { // Manual installation or development // Get deployment type for restart instructions deploymentType := updates.GetDeploymentType() response := map[string]interface{}{ "success": true, "method": "manual", "envFile": envPath, "deploymentType": deploymentType, "requiresManualRestart": false, "message": "Security enabled immediately! Your settings are saved and active.", "note": fmt.Sprintf("Configuration saved to %s for persistence across restarts.", envPath), } w.Header().Set("Content-Type", "application/json") setupCompleted = true json.NewEncoder(w).Encode(response) } } } // HandleRegenerateAPIToken generates and persists a new API token. func (r *Router) HandleRegenerateAPIToken(w http.ResponseWriter, rq *http.Request) { if rq.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if !CheckAuth(r.config, w, rq) { return } // Check proxy auth for admin status if r.config.ProxyAuthSecret != "" { if valid, username, isAdmin := CheckProxyAuth(r.config, rq); valid && !isAdmin { log.Warn(). Str("ip", GetClientIP(rq)). Str("username", username). Msg("Non-admin user attempted API token regeneration") http.Error(w, "Admin privileges required", http.StatusForbidden) return } } if !ensureSettingsWriteScope(r.config, w, rq) { return } // Apply rate limiting to prevent abuse clientIP := GetClientIP(rq) if !authLimiter.Allow(clientIP) { log.Warn().Str("ip", clientIP).Msg("Rate limit exceeded for API token generation") http.Error(w, "Too many attempts. Please try again later.", http.StatusTooManyRequests) return } // Generate new token using the auth package rawToken, err := internalauth.GenerateAPIToken() if err != nil { log.Error().Err(err).Msg("Failed to generate API token") http.Error(w, "Failed to generate token", http.StatusInternalServerError) return } tokenRecord, err := config.NewAPITokenRecord(rawToken, "Regenerated token", nil) if err != nil { log.Error().Err(err).Msg("Failed to construct API token record") http.Error(w, "Failed to generate token", http.StatusInternalServerError) return } setAPITokenOwnerUserID(tokenRecord, apiTokenOwnerUserIDForRequest(r.config, rq)) config.Mu.Lock() previousTokens := append([]config.APITokenRecord(nil), r.config.APITokens...) r.config.APITokens = []config.APITokenRecord{*tokenRecord} r.config.SortAPITokens() if r.persistence != nil { if err := r.persistence.SaveAPITokens(r.config.APITokens); err != nil { r.config.APITokens = previousTokens r.config.SortAPITokens() config.Mu.Unlock() log.Error().Err(err).Msg("Failed to persist regenerated API token") http.Error(w, "Failed to save token", http.StatusInternalServerError) return } } config.Mu.Unlock() log.Info().Msg("Runtime config updated with new API token - active immediately") log.Info().Msg("API token regenerated successfully") // Get deployment type for restart instructions deploymentType := updates.GetDeploymentType() response := map[string]interface{}{ "success": true, "token": rawToken, // Return the raw token to the user (only shown once!) "deploymentType": deploymentType, "requiresRestart": false, "message": "New API token generated and active immediately! Save this token - it won't be shown again.", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleValidateAPIToken validates an API token without logging it func (r *Router) HandleValidateAPIToken(w http.ResponseWriter, rq *http.Request) { if rq.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Require authentication to prevent unauthenticated token guessing oracle if !CheckAuth(r.config, w, rq) { return } // Check proxy auth for admin status if r.config.ProxyAuthSecret != "" { if valid, username, isAdmin := CheckProxyAuth(r.config, rq); valid && !isAdmin { log.Warn(). Str("ip", GetClientIP(rq)). Str("username", username). Msg("Non-admin user attempted API token validation") http.Error(w, "Admin privileges required", http.StatusForbidden) return } } if !ensureSettingsWriteScope(r.config, w, rq) { return } // Apply rate limiting to prevent brute force attacks clientIP := GetClientIP(rq) if !authLimiter.Allow(clientIP) { log.Warn().Str("ip", clientIP).Msg("Rate limit exceeded for API token validation") http.Error(w, "Too many attempts. Please try again later.", http.StatusTooManyRequests) return } // Parse request body var validateRequest struct { Token string `json:"token"` } if err := decodeSecurityRequestBody(w, rq, &validateRequest); err != nil { http.Error(w, "Invalid request body", securityRequestErrorStatus(err)) return } if validateRequest.Token == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]interface{}{ "valid": false, "message": "Token is required", }) return } // Check if API token auth is enabled if !r.config.HasAPITokens() { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "valid": false, "message": "API token authentication is not configured", }) return } // Validate the token (compare hash) config.Mu.RLock() _, isValid := r.config.ValidateAPIToken(validateRequest.Token) config.Mu.RUnlock() // Log validation attempt without logging the token itself if isValid { log.Debug(). Str("ip", clientIP). Msg("API token validation successful") } else { log.Warn(). Str("ip", clientIP). Msg("API token validation failed") } // Return validation result response := map[string]interface{}{ "valid": isValid, } if isValid { response["message"] = "Token is valid" } else { response["message"] = "Token is invalid" } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }