From da6dc52a911472a8ea64918eba391c07e00151ed Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Tue, 12 Aug 2025 20:10:21 +0000 Subject: [PATCH] feat: add Quick Security Setup wizard for one-click security hardening - Created QuickSecuritySetup component with password/token generation - Added /api/security/quick-setup endpoint to generate config - Shows credentials once with copy/download functionality - Generates systemd environment configuration file - Only shows when authentication is not already enabled --- .../Settings/QuickSecuritySetup.tsx | 251 ++++++++++++++++++ .../src/components/Settings/Settings.tsx | 12 + internal/api/router.go | 64 +++++ 3 files changed, 327 insertions(+) create mode 100644 frontend-modern/src/components/Settings/QuickSecuritySetup.tsx diff --git a/frontend-modern/src/components/Settings/QuickSecuritySetup.tsx b/frontend-modern/src/components/Settings/QuickSecuritySetup.tsx new file mode 100644 index 000000000..cabbfe8ec --- /dev/null +++ b/frontend-modern/src/components/Settings/QuickSecuritySetup.tsx @@ -0,0 +1,251 @@ +import { Component, createSignal, Show } from 'solid-js'; +import { showSuccess, showError } from '@/utils/toast'; + +interface SecurityCredentials { + username: string; + password: string; + apiToken?: string; +} + +export const QuickSecuritySetup: Component = () => { + const [isSettingUp, setIsSettingUp] = createSignal(false); + const [credentials, setCredentials] = createSignal(null); + const [showCredentials, setShowCredentials] = createSignal(false); + const [copied, setCopied] = createSignal<'username' | 'password' | 'token' | null>(null); + + const generatePassword = (length: number = 16): string => { + const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'; + let password = ''; + const array = new Uint8Array(length); + crypto.getRandomValues(array); + for (let i = 0; i < length; i++) { + password += charset[array[i] % charset.length]; + } + return password; + }; + + const generateToken = (): string => { + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); + }; + + const copyToClipboard = async (text: string, type: 'username' | 'password' | 'token') => { + try { + await navigator.clipboard.writeText(text); + setCopied(type); + setTimeout(() => setCopied(null), 2000); + } catch (err) { + showError('Failed to copy to clipboard'); + } + }; + + const setupSecurity = async () => { + setIsSettingUp(true); + + try { + // Generate credentials + const newCredentials: SecurityCredentials = { + username: 'admin', + password: generatePassword(), + apiToken: generateToken() + }; + + // Call API to enable security + const response = await fetch('/api/security/quick-setup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newCredentials) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || 'Failed to setup security'); + } + + setCredentials(newCredentials); + setShowCredentials(true); + showSuccess('Security enabled successfully!'); + } catch (error) { + showError(`Failed to setup security: ${error}`); + } finally { + setIsSettingUp(false); + } + }; + + const downloadCredentials = () => { + if (!credentials()) return; + + const content = `Pulse Security Credentials +Generated: ${new Date().toISOString()} + +Basic Authentication: +Username: ${credentials()!.username} +Password: ${credentials()!.password} + +API Token: ${credentials()!.apiToken} + +Important: +- Save these credentials securely +- They will not be shown again +- Use the API token for export/import operations +- Basic auth is required to access the web interface +`; + + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `pulse-credentials-${Date.now()}.txt`; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+ +
+
+
+ + + +
+
+

Quick Security Setup

+

+ Enable authentication with one click. This will: +

+
    +
  • + + Generate secure random password +
  • +
  • + + Enable basic authentication +
  • +
  • + + Create API token for automation +
  • +
  • + + Enable audit logging +
  • +
+
+
+ +
+
+ + + +
+

Important:

+

Credentials will be shown only once. Save them immediately!

+
+
+
+ + +
+
+ + +
+
+

+ 🎉 Security Enabled Successfully! +

+ +
+ +
+

+ Save these credentials now - they won't be shown again! +

+
+ +
+
+ +
+ + {credentials()!.username} + + +
+
+ +
+ +
+ + {credentials()!.password} + + +
+
+ +
+ +
+ + {credentials()!.apiToken} + + +
+
+
+ +
+

+ Next steps: You'll need to restart Pulse with these environment variables set. + See the documentation for systemd or Docker configuration. +

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index 49d1472ac..b1e9bd3ac 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -4,6 +4,7 @@ import { showSuccess, showError } from '@/utils/toast'; import { NodeModal } from './NodeModal'; import RegistrationTokens from './RegistrationTokens'; import { APITokenManager } from './APITokenManager'; +import { QuickSecuritySetup } from './QuickSecuritySetup'; import { SettingsAPI } from '@/api/settings'; import { NodesAPI } from '@/api/nodes'; import { UpdatesAPI } from '@/api/updates'; @@ -118,6 +119,10 @@ const Settings: Component = () => { requiresAuth: boolean; exportProtected: boolean; unprotectedExportAllowed: boolean; + hasAuthentication: boolean; + hasAuditLogging: boolean; + credentialsEncrypted: boolean; + hasHTTPS: boolean; } | null>(null); const [exportPassphrase, setExportPassphrase] = createSignal(''); const [importPassphrase, setImportPassphrase] = createSignal(''); @@ -1471,6 +1476,13 @@ const Settings: Component = () => { {/* Security Tab */}
+ +
+

Quick Security Setup

+ +
+
+

API Security

diff --git a/internal/api/router.go b/internal/api/router.go index ce954b521..c9e68db41 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -221,6 +221,70 @@ func (r *Router) setupRoutes() { } }) + // Quick security setup route + r.mux.HandleFunc("/api/security/quick-setup", func(w http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPost { + // Parse request body + var setupRequest struct { + Username string `json:"username"` + Password string `json:"password"` + APIToken string `json:"apiToken"` + } + + if err := json.NewDecoder(req.Body).Decode(&setupRequest); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Validate inputs + if setupRequest.Username == "" || setupRequest.Password == "" || setupRequest.APIToken == "" { + http.Error(w, "Username, password, and API token are required", http.StatusBadRequest) + return + } + + // Create config file for systemd environment + configContent := fmt.Sprintf(`# Pulse Security Configuration +# Generated by Quick Security Setup on %s +# +# Add these to your systemd service configuration: +# sudo systemctl edit pulse-backend +# +# [Service] +# Environment="PULSE_AUTH_USER=%s" +# Environment="PULSE_AUTH_PASS=%s" +# Environment="API_TOKEN=%s" +# Environment="ENABLE_AUDIT_LOG=true" +# +# Then restart the service: +# sudo systemctl restart pulse-backend + +PULSE_AUTH_USER=%s +PULSE_AUTH_PASS=%s +API_TOKEN=%s +ENABLE_AUDIT_LOG=true +`, time.Now().Format(time.RFC3339), setupRequest.Username, setupRequest.Password, setupRequest.APIToken, + setupRequest.Username, setupRequest.Password, setupRequest.APIToken) + + // Save to a temporary config file for user reference + configPath := fmt.Sprintf("%s/security-config-%d.env", r.config.DataPath, time.Now().Unix()) + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { + log.Error().Err(err).Msg("Failed to write security config file") + } + + // Return success with instructions + response := map[string]interface{}{ + "success": true, + "configPath": configPath, + "instructions": "Security configuration has been generated. Please update your systemd service configuration with the provided environment variables and restart Pulse.", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + } else { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + }) + // Config export/import routes (requires API token for security) r.mux.HandleFunc("/api/config/export", r.exportLimiter.Middleware(func(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodPost {