diff --git a/frontend-modern/src/api/system.ts b/frontend-modern/src/api/system.ts index da99c8081..a180db08d 100644 --- a/frontend-modern/src/api/system.ts +++ b/frontend-modern/src/api/system.ts @@ -1,53 +1,16 @@ -// System API for managing system settings including API tokens +// System API for managing system settings import { apiFetchJSON } from '@/utils/apiClient'; -export interface APITokenStatus { - hasToken: boolean; - token?: string; -} - export interface SystemSettings { pollingInterval: number; updateChannel?: string; autoUpdateEnabled: boolean; autoUpdateCheckInterval?: number; autoUpdateTime?: string; - apiToken?: string; + // apiToken removed - now handled via security API } export class SystemAPI { - // API Token Management - static async getAPITokenStatus(): Promise { - return apiFetchJSON('/api/system/api-token'); - } - - static async getAPIToken(reveal: boolean = false): Promise { - const url = reveal ? '/api/system/api-token?reveal=true' : '/api/system/api-token'; - return apiFetchJSON(url); - } - - static async generateAPIToken(): Promise { - const result = await apiFetchJSON('/api/system/api-token/generate', { - method: 'POST', - }); - - // Store the new token locally - if (result.token) { - localStorage.setItem('apiToken', result.token); - } - - return result; - } - - static async deleteAPIToken(): Promise { - await apiFetchJSON('/api/system/api-token/delete', { - method: 'DELETE', - }); - - // Clear local storage - localStorage.removeItem('apiToken'); - } - // System Settings static async getSystemSettings(): Promise { return apiFetchJSON('/api/system/settings'); diff --git a/frontend-modern/src/components/Settings/APITokenManager.tsx b/frontend-modern/src/components/Settings/APITokenManager.tsx deleted file mode 100644 index 2c66bc1ef..000000000 --- a/frontend-modern/src/components/Settings/APITokenManager.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { createSignal, Show, onMount } from 'solid-js'; -import { SystemAPI, APITokenStatus } from '@/api/system'; -import { copyToClipboard } from '@/utils/clipboard'; - -export function APITokenManager() { - const [tokenStatus, setTokenStatus] = createSignal(null); - const [loading, setLoading] = createSignal(false); - const [showToken, setShowToken] = createSignal(false); - const [currentToken, setCurrentToken] = createSignal(null); - const [error, setError] = createSignal(null); - const [copied, setCopied] = createSignal(false); - const [showDeleteConfirm, setShowDeleteConfirm] = createSignal(false); - - // Load initial status and fetch the actual token if it exists - onMount(async () => { - try { - const status = await SystemAPI.getAPITokenStatus(); - setTokenStatus(status); - - // If there's a token, fetch it immediately - if (status.hasToken) { - try { - const tokenData = await SystemAPI.getAPIToken(true); - if (tokenData.token) { - setCurrentToken(tokenData.token); - setShowToken(true); - } - } catch (err) { - console.error('Failed to fetch existing API token:', err); - } - } - } catch (err) { - console.error('Failed to load API token status:', err); - } - }); - - const generateToken = async () => { - setLoading(true); - setError(null); - try { - const result = await SystemAPI.generateAPIToken(); - setTokenStatus(result); - setCurrentToken(result.token || null); - setShowToken(true); - } catch (err: any) { - setError(err.message || 'Failed to generate token'); - } finally { - setLoading(false); - } - }; - - const deleteToken = async () => { - setLoading(true); - setError(null); - try { - await SystemAPI.deleteAPIToken(); - setTokenStatus({ hasToken: false }); - setCurrentToken(null); - setShowToken(false); - setShowDeleteConfirm(false); - } catch (err: any) { - setError(err.message || 'Failed to delete token'); - } finally { - setLoading(false); - } - }; - - const handleCopy = async () => { - if (!currentToken()) return; - - const success = await copyToClipboard(currentToken()!); - if (success) { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } else { - setError('Failed to copy - please select and copy manually'); - } - }; - - return ( -
- - -
-

{error()}

-
-
- - -

- No token configured -

- - -
- } - > -
-
-

- Token active -

- -
- - -
-

- API Token -

-
- e.currentTarget.select()} - /> - -
-
-

Use this token for API authentication:

- - curl -H "X-API-Token: {currentToken()}" {window.location.origin}/api/health - -
-
-
- -
- - - -
- -
- - - {/* Delete Confirmation Modal */} - -
-
-

- Remove API Token? -

-

- This will remove API authentication from your Pulse instance. All configuration endpoints - will be accessible without credentials. -

-
- - -
-
-
-
- - ); -} \ No newline at end of file diff --git a/internal/api/router.go b/internal/api/router.go index 4308a0a80..1de1ab409 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -601,9 +601,7 @@ func (r *Router) setupRoutes() { systemSettingsHandler := NewSystemSettingsHandler(r.config, r.persistence) r.mux.HandleFunc("/api/system/settings", systemSettingsHandler.HandleGetSystemSettings) r.mux.HandleFunc("/api/system/settings/update", systemSettingsHandler.HandleUpdateSystemSettings) - r.mux.HandleFunc("/api/system/api-token", systemSettingsHandler.HandleGetAPIToken) - r.mux.HandleFunc("/api/system/api-token/generate", systemSettingsHandler.HandleGenerateAPIToken) - r.mux.HandleFunc("/api/system/api-token/delete", systemSettingsHandler.HandleDeleteAPIToken) + // Old API token endpoints removed - now using /api/security/regenerate-token // WebSocket endpoint r.mux.HandleFunc("/ws", r.handleWebSocket) diff --git a/internal/api/system_settings.go b/internal/api/system_settings.go index 711b6fe1e..013271b46 100644 --- a/internal/api/system_settings.go +++ b/internal/api/system_settings.go @@ -1,18 +1,15 @@ package api import ( - "crypto/rand" - "encoding/hex" "encoding/json" "net/http" - "os" "time" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rs/zerolog/log" ) -// SystemSettingsHandler handles system settings including API token management +// SystemSettingsHandler handles system settings type SystemSettingsHandler struct { config *config.Config persistence *config.ConfigPersistence @@ -26,147 +23,7 @@ func NewSystemSettingsHandler(cfg *config.Config, persistence *config.ConfigPers } } -// APITokenResponse represents the API token response -type APITokenResponse struct { - HasToken bool `json:"hasToken"` - Token string `json:"token,omitempty"` -} - -// HandleGetAPIToken returns the current API token status -func (h *SystemSettingsHandler) HandleGetAPIToken(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Check if API token is set - hasToken := h.config.APIToken != "" - - response := APITokenResponse{ - HasToken: hasToken, - } - - // Only return the token if it's requested with proper auth - if hasToken && r.URL.Query().Get("reveal") == "true" { - // Verify the request is authenticated (session, password, or API token) - if CheckAuth(h.config, w, r) { - response.Token = h.config.APIToken - } else { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -// HandleGenerateAPIToken generates a new API token -func (h *SystemSettingsHandler) HandleGenerateAPIToken(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Use standard auth check if any authentication is configured - // This allows generation via web UI when logged in with password - if !CheckAuth(h.config, w, r) { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // Generate a new secure token - tokenBytes := make([]byte, 32) - if _, err := rand.Read(tokenBytes); err != nil { - log.Error().Err(err).Msg("Failed to generate random token") - http.Error(w, "Failed to generate token", http.StatusInternalServerError) - return - } - - newToken := hex.EncodeToString(tokenBytes) - - // Save to system settings - settings, err := h.persistence.LoadSystemSettings() - if err != nil { - log.Error().Err(err).Msg("Failed to load system settings") - settings = &config.SystemSettings{} - } - - settings.APIToken = newToken - - if err := h.persistence.SaveSystemSettings(*settings); err != nil { - log.Error().Err(err).Msg("Failed to save system settings") - http.Error(w, "Failed to save token", http.StatusInternalServerError) - return - } - - // Update the running config - h.config.APIToken = newToken - - // Don't override if env var is set - if os.Getenv("API_TOKEN") != "" { - log.Warn().Msg("API_TOKEN environment variable is set and will override UI-configured token on restart") - } - - log.Info().Msg("API token generated via UI") - - response := APITokenResponse{ - HasToken: true, - Token: newToken, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -// HandleDeleteAPIToken removes the API token -func (h *SystemSettingsHandler) HandleDeleteAPIToken(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Use standard auth check (allows session, password, or API token) - // This allows deletion via web UI when logged in with password - if !CheckAuth(h.config, w, r) { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // Save to system settings - settings, err := h.persistence.LoadSystemSettings() - if err != nil { - log.Error().Err(err).Msg("Failed to load system settings") - settings = &config.SystemSettings{} - } - - settings.APIToken = "" - - if err := h.persistence.SaveSystemSettings(*settings); err != nil { - log.Error().Err(err).Msg("Failed to save system settings") - http.Error(w, "Failed to remove token", http.StatusInternalServerError) - return - } - - // Update the running config - h.config.APIToken = "" - - // Warn if env var is set - if os.Getenv("API_TOKEN") != "" { - log.Warn().Msg("API_TOKEN environment variable is set and will override this change on restart") - } - - log.Info().Msg("API token removed via UI") - - response := APITokenResponse{ - HasToken: false, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -// HandleGetSystemSettings returns all system settings +// HandleGetSystemSettings returns the current system settings func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -176,29 +33,24 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r settings, err := h.persistence.LoadSystemSettings() if err != nil { log.Error().Err(err).Msg("Failed to load system settings") - http.Error(w, "Failed to load settings", http.StatusInternalServerError) - return + settings = &config.SystemSettings{ + PollingInterval: 5, + } } - - // Don't expose the actual token in this endpoint - if settings.APIToken != "" { - settings.APIToken = "***HIDDEN***" - } - + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(settings) } -// HandleUpdateSystemSettings updates system settings +// HandleUpdateSystemSettings updates the system settings func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - // Use standard auth check (allows session, password, or API token) + // Require authentication if !CheckAuth(h.config, w, r) { - http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -207,26 +59,25 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter http.Error(w, "Invalid request body", http.StatusBadRequest) return } - - // Don't allow updating API token through this endpoint - existingSettings, _ := h.persistence.LoadSystemSettings() - if existingSettings != nil { - settings.APIToken = existingSettings.APIToken - } - - if err := h.persistence.SaveSystemSettings(settings); err != nil { - log.Error().Err(err).Msg("Failed to save system settings") - http.Error(w, "Failed to save settings", http.StatusInternalServerError) - return - } - - // Update relevant config fields + + // Load existing settings to preserve fields not in the request + // (removed - not needed without API token preservation) + + // Update the config if settings.PollingInterval > 0 { h.config.PollingInterval = time.Duration(settings.PollingInterval) * time.Second } + if settings.AllowedOrigins != "" { + h.config.AllowedOrigins = settings.AllowedOrigins + } + if settings.ConnectionTimeout > 0 { + h.config.ConnectionTimeout = time.Duration(settings.ConnectionTimeout) * time.Second + } if settings.UpdateChannel != "" { h.config.UpdateChannel = settings.UpdateChannel } + + // Update auto-update settings h.config.AutoUpdateEnabled = settings.AutoUpdateEnabled if settings.AutoUpdateCheckInterval > 0 { h.config.AutoUpdateCheckInterval = time.Duration(settings.AutoUpdateCheckInterval) * time.Hour @@ -234,9 +85,16 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter if settings.AutoUpdateTime != "" { h.config.AutoUpdateTime = settings.AutoUpdateTime } - - log.Info().Msg("System settings updated via UI") - + + // Save to persistence + if err := h.persistence.SaveSystemSettings(settings); err != nil { + log.Error().Err(err).Msg("Failed to save system settings") + http.Error(w, "Failed to save settings", http.StatusInternalServerError) + return + } + + log.Info().Msg("System settings updated") + w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + json.NewEncoder(w).Encode(map[string]bool{"success": true}) } \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 759c05f0c..b8319c1b9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -229,13 +229,10 @@ func Load() (*Config, error) { if systemSettings.ConnectionTimeout > 0 { cfg.ConnectionTimeout = time.Duration(systemSettings.ConnectionTimeout) * time.Second } - if systemSettings.APIToken != "" { - cfg.APIToken = systemSettings.APIToken - } + // APIToken no longer loaded from system.json - only from .env log.Info(). Dur("interval", cfg.PollingInterval). Str("updateChannel", cfg.UpdateChannel). - Bool("hasAPIToken", cfg.APIToken != ""). Msg("Loaded system configuration") } } diff --git a/internal/config/persistence.go b/internal/config/persistence.go index c6f4cfa37..b5af999d5 100644 --- a/internal/config/persistence.go +++ b/internal/config/persistence.go @@ -295,7 +295,7 @@ type SystemSettings struct { AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"` AutoUpdateTime string `json:"autoUpdateTime,omitempty"` - APIToken string `json:"apiToken,omitempty"` + // APIToken removed - now handled via .env file only } // SaveNodesConfig saves nodes configuration to file (encrypted)