diff --git a/internal/api/auth.go b/internal/api/auth.go index 85839b2..cc71469 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -196,6 +196,9 @@ func min(a, b int) int { // CheckAuth checks both basic auth and API token func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool { + config.Mu.RLock() + defer config.Mu.RUnlock() + // Check proxy auth first if configured if cfg.ProxyAuthSecret != "" { if valid, username, _ := CheckProxyAuth(cfg, r); valid { diff --git a/internal/api/router.go b/internal/api/router.go index 42ad246..481a193 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1201,6 +1201,9 @@ func (r *Router) SetConfig(cfg *config.Config) { return } + config.Mu.Lock() + defer config.Mu.Unlock() + if r.config == nil { r.config = cfg } else { @@ -2839,6 +2842,9 @@ func (r *Router) handleConfig(w http.ResponseWriter, req *http.Request) { return } + config.Mu.RLock() + defer config.Mu.RUnlock() + // Return public configuration config := map[string]interface{}{ "csrfProtection": false, // Not implemented yet diff --git a/internal/api/security_setup_fix.go b/internal/api/security_setup_fix.go index 9580d9b..b4f01ed 100644 --- a/internal/api/security_setup_fix.go +++ b/internal/api/security_setup_fix.go @@ -275,11 +275,13 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc { } // Update runtime config immediately with hashed token - no restart needed! + config.Mu.Lock() r.config.AuthUser = setupRequest.Username r.config.AuthPass = hashedPassword r.config.APITokens = []config.APITokenRecord{*tokenRecord} r.config.SortAPITokens() r.config.APITokenEnabled = true + config.Mu.Unlock() if r.persistence != nil { if err := r.persistence.SaveAPITokens(r.config.APITokens); err != nil { @@ -539,9 +541,11 @@ func (r *Router) HandleRegenerateAPIToken(w http.ResponseWriter, rq *http.Reques return } + config.Mu.Lock() r.config.APITokens = []config.APITokenRecord{*tokenRecord} r.config.SortAPITokens() r.config.APITokenEnabled = true + config.Mu.Unlock() log.Info().Msg("Runtime config updated with new API token - active immediately") if r.persistence != nil { @@ -672,7 +676,9 @@ func (r *Router) HandleValidateAPIToken(w http.ResponseWriter, rq *http.Request) } // 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 { diff --git a/internal/api/security_tokens.go b/internal/api/security_tokens.go index 7e1d586..fb8875a 100644 --- a/internal/api/security_tokens.go +++ b/internal/api/security_tokens.go @@ -91,6 +91,9 @@ func (r *Router) handleListAPITokens(w http.ResponseWriter, req *http.Request) { return } + config.Mu.RLock() + defer config.Mu.RUnlock() + tokens := make([]apiTokenDTO, 0, len(r.config.APITokens)) for _, record := range r.config.APITokens { tokens = append(tokens, toAPITokenDTO(record)) @@ -147,6 +150,9 @@ func (r *Router) handleCreateAPIToken(w http.ResponseWriter, req *http.Request) return } + config.Mu.Lock() + defer config.Mu.Unlock() + r.config.APITokens = append(r.config.APITokens, *record) r.config.SortAPITokens() r.config.APITokenEnabled = true @@ -181,6 +187,9 @@ func (r *Router) handleDeleteAPIToken(w http.ResponseWriter, req *http.Request) return } + config.Mu.Lock() + defer config.Mu.Unlock() + removed := r.config.RemoveAPIToken(id) if !removed { http.Error(w, "Token not found", http.StatusNotFound) diff --git a/internal/config/config.go b/internal/config/config.go index 4fbc657..d1869d0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,8 @@ import ( "strings" "time" + "sync" + "github.com/google/uuid" "github.com/joho/godotenv" "github.com/rcourtman/pulse-go-rewrite/internal/auth" @@ -29,6 +31,10 @@ import ( "github.com/rs/zerolog/log" ) +// Mu protects concurrent access to the configuration, +// particularly for fields updated by the config watcher (AuthUser, AuthPass, APITokens). +var Mu sync.RWMutex + const ( DefaultGuestMetadataMinRefresh = 2 * time.Minute DefaultGuestMetadataRefreshJitter = 45 * time.Second diff --git a/internal/config/watcher.go b/internal/config/watcher.go index 57ee6f1..754af31 100644 --- a/internal/config/watcher.go +++ b/internal/config/watcher.go @@ -310,6 +310,9 @@ func (cw *ConfigWatcher) reloadConfig() { } // Apply auth user + Mu.Lock() + defer Mu.Unlock() + newUser := strings.Trim(envMap["PULSE_AUTH_USER"], "'\"") if newUser != oldAuthUser { cw.config.AuthUser = newUser @@ -485,6 +488,8 @@ func (cw *ConfigWatcher) reloadAPITokens() { } // Only update if we successfully loaded tokens + Mu.Lock() + defer Mu.Unlock() cw.config.APITokens = tokens cw.config.SortAPITokens() cw.config.APITokenEnabled = len(tokens) > 0