diff --git a/frontend-modern/src/components/FirstRunSetup.tsx b/frontend-modern/src/components/FirstRunSetup.tsx index c07f5e1..e9652c1 100644 --- a/frontend-modern/src/components/FirstRunSetup.tsx +++ b/frontend-modern/src/components/FirstRunSetup.tsx @@ -22,6 +22,9 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool const [savedToken, setSavedToken] = createSignal(''); const [copied, setCopied] = createSignal<'password' | 'token' | null>(null); const [themeMode, setThemeMode] = createSignal<'system' | 'light' | 'dark'>('system'); + const [bootstrapToken, setBootstrapToken] = createSignal(''); + const [isUnlocking, setIsUnlocking] = createSignal(false); + const [isUnlocked, setIsUnlocked] = createSignal(false); const applyTheme = (mode: 'system' | 'light' | 'dark') => { if (mode === 'light') { @@ -77,6 +80,15 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(''); }; + const handleUnlock = () => { + if (!bootstrapToken().trim()) { + showError('Please enter the bootstrap token'); + return; + } + // Simple client-side unlock - actual validation happens during setup + setIsUnlocked(true); + }; + const handleSetup = async () => { // Validate custom password if used if (useCustomPassword()) { @@ -104,15 +116,23 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool setApiClientToken(token); try { + const headers: Record = { 'Content-Type': 'application/json' }; + + // Include bootstrap token if we're in first-run setup (not force mode) + if (!props.force && bootstrapToken()) { + headers['X-Setup-Token'] = bootstrapToken().trim(); + } + const response = await fetch('/api/security/quick-setup', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers, credentials: 'include', // Include cookies for CSRF body: JSON.stringify({ username: username(), password: finalPassword, apiToken: token, force: props.force ?? false, + setupToken: bootstrapToken().trim(), // Also include in body as fallback }), }); @@ -251,7 +271,78 @@ IMPORTANT: Keep these credentials secure!
- + {/* Bootstrap Token Unlock Screen */} + +
+ + +
+ {/* Instructions */} +
+

+ To begin setup, retrieve the bootstrap token from your Pulse host: +

+
+
+
# Standard installation:
+ cat /etc/pulse/.bootstrap_token +
+
+
# Docker/Helm:
+ cat /data/.bootstrap_token +
+
+
+ + {/* Token Input */} +
+ + setBootstrapToken(e.currentTarget.value)} + onKeyPress={(e) => e.key === 'Enter' && handleUnlock()} + class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm" + placeholder="Paste the token from your host" + autofocus + /> +

+ This one-time token ensures only someone with host access can configure Pulse +

+
+ + {/* Security Note */} +
+

+ Why this step? +
+ The bootstrap token prevents unauthorized access to your unconfigured Pulse instance. + It's automatically removed after you complete the setup wizard. +

+
+ + {/* Unlock Button */} + +
+
+
+ + {/* Setup Form - only shown after unlock or in force mode */} +