import { Component, createSignal, Show, onMount } from 'solid-js'; import { showSuccess, showError } from '@/utils/toast'; import { copyToClipboard } from '@/utils/clipboard'; import { clearAuth as clearApiClientAuth, setApiToken as setApiClientToken } from '@/utils/apiClient'; import { getPulseBaseUrl } from '@/utils/url'; import { SectionHeader } from '@/components/shared/SectionHeader'; import { showTokenReveal } from '@/stores/tokenReveal'; import type { APITokenRecord } from '@/api/security'; import { STORAGE_KEYS } from '@/utils/localStorage'; export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: boolean }> = ( props, ) => { const [username, setUsername] = createSignal('admin'); const [password, setPassword] = createSignal(''); const [confirmPassword, setConfirmPassword] = createSignal(''); const [useCustomPassword, setUseCustomPassword] = createSignal(false); const [isSettingUp, setIsSettingUp] = createSignal(false); const [showCredentials, setShowCredentials] = createSignal(false); const [savedUsername, setSavedUsername] = createSignal(''); const [savedPassword, setSavedPassword] = createSignal(''); 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 [isUnlocked, setIsUnlocked] = createSignal(false); const [bootstrapTokenPath, setBootstrapTokenPath] = createSignal(''); const [isDocker, setIsDocker] = createSignal(false); const [inContainer, setInContainer] = createSignal(false); const [lxcCtid, setLxcCtid] = createSignal(''); const [dockerContainerName, setDockerContainerName] = createSignal(''); const [showAlternatives, setShowAlternatives] = createSignal(false); const [isValidatingToken, setIsValidatingToken] = createSignal(false); const applyTheme = (mode: 'system' | 'light' | 'dark') => { if (mode === 'light') { document.documentElement.classList.remove('dark'); localStorage.setItem(STORAGE_KEYS.DARK_MODE, 'false'); } else if (mode === 'dark') { document.documentElement.classList.add('dark'); localStorage.setItem(STORAGE_KEYS.DARK_MODE, 'true'); } else { // System preference localStorage.removeItem(STORAGE_KEYS.DARK_MODE); if (window.matchMedia('(prefers-color-scheme: dark)').matches) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } } }; onMount(async () => { // Check for saved theme preference const savedTheme = localStorage.getItem(STORAGE_KEYS.DARK_MODE); if (savedTheme === 'false') { setThemeMode('light'); document.documentElement.classList.remove('dark'); } else if (savedTheme === 'true') { setThemeMode('dark'); document.documentElement.classList.add('dark'); } else { // No saved preference - use system preference setThemeMode('system'); if (window.matchMedia('(prefers-color-scheme: dark)').matches) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } } // Fetch bootstrap token path from API try { const response = await fetch('/api/security/status'); if (response.ok) { const data = await response.json(); if (data.bootstrapTokenPath) { setBootstrapTokenPath(data.bootstrapTokenPath); setIsDocker(data.isDocker || false); setInContainer(data.inContainer || false); setLxcCtid(data.lxcCtid || ''); setDockerContainerName(data.dockerContainerName || ''); } } } catch (error) { console.error('Failed to fetch bootstrap token path:', error); } }); const generatePassword = () => { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%'; let pass = ''; for (let i = 0; i < 16; i++) { pass += chars.charAt(Math.floor(Math.random() * chars.length)); } return pass; }; const generateToken = (): string => { // Generate 24 bytes (48 hex chars) to avoid hash detection issue const array = new Uint8Array(24); crypto.getRandomValues(array); return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(''); }; const handleUnlock = async () => { if (!bootstrapToken().trim()) { showError('Please enter the bootstrap token'); return; } setIsValidatingToken(true); try { const response = await fetch('/api/security/validate-bootstrap-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: bootstrapToken().trim() }), }); if (!response.ok) { const error = await response.text(); throw new Error(error || 'Invalid bootstrap setup token'); } setIsUnlocked(true); showSuccess('Bootstrap token verified. Continue with setup.'); } catch (error) { if (error instanceof Error) { showError(error.message || 'Failed to validate bootstrap token'); } else { showError('Failed to validate bootstrap token'); } } finally { setIsValidatingToken(false); } }; const handleSetup = async () => { // Validate custom password if used if (useCustomPassword()) { if (!password()) { showError('Please enter a password'); return; } if (password() !== confirmPassword()) { showError('Passwords do not match'); return; } if (password().length < 12) { showError('Password must be at least 12 characters'); return; } } setIsSettingUp(true); // Generate password if not custom const finalPassword = useCustomPassword() ? password() : generatePassword(); // Generate API token const token = generateToken(); 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, 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 }), }); if (!response.ok) { const error = await response.text(); throw new Error(error || 'Failed to setup security'); } const result = await response.json(); if (result.skipped) { // Shouldn't happen in first-run, but handle it window.location.reload(); return; } // Save credentials for display setSavedUsername(username()); setSavedPassword(finalPassword); setSavedToken(token); // Clear any cached credentials from prior sessions so a reload doesn't auto-submit again clearApiClientAuth(); const bootstrapRecord: APITokenRecord = { id: 'bootstrap-token', name: 'Bootstrap token', prefix: token.slice(0, 6), suffix: token.slice(-4), createdAt: new Date().toISOString(), }; showTokenReveal({ token, record: bootstrapRecord, source: 'first-run', note: 'Copy this bootstrap token now. It unlocks API access for agents and automations.', }); // Show credentials setShowCredentials(true); showSuccess('Security configured successfully!'); } catch (error) { showError(`Failed to setup security: ${error}`); } finally { setIsSettingUp(false); } }; const handleCopy = async (type: 'password' | 'token') => { const value = type === 'password' ? savedPassword() : savedToken(); const success = await copyToClipboard(value); if (success) { setCopied(type); setTimeout(() => setCopied(null), 2000); } }; const downloadCredentials = () => { const baseUrl = getPulseBaseUrl(); const credentials = `Pulse Security Credentials ======================== Generated: ${new Date().toISOString()} Web Interface Login: ------------------- URL: ${baseUrl} Username: ${savedUsername()} Password: ${savedPassword()} API Access: ----------- API Token: ${savedToken()} Example API Usage: curl -H "X-API-Token: ${savedToken()}" ${baseUrl}/api/state IMPORTANT: Keep these credentials secure! `; const blob = new Blob([credentials], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `pulse-credentials-${Date.now()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; return (

Authentication forced off via environment

Pulse detected the legacy DISABLE_AUTH flag. Complete the setup below to rotate credentials, then remove the environment variable and restart Pulse.

If you still need a temporary bypass after rotating, create .auth_recovery in the Pulse data directory and restart. Remove the file and restart again once you regain access.

{/* Logo/Header */}
Pulse

Let's set up your monitoring dashboard

{/* 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
} > {/* Show most relevant command based on detected environment */}
{/* LXC with detected CTID */}
# Run from Proxmox host:
pct exec {lxcCtid()} -- cat {bootstrapTokenPath()}
{/* LXC without CTID */}
# Run from Proxmox host:
pct exec <ctid> -- cat {bootstrapTokenPath()}
{/* Docker (with detected name or placeholder) */}
# From Docker host:
docker exec {dockerContainerName() || ''} cat {bootstrapTokenPath()}
{/* Bare metal / inside container */}
# On this host:
cat {bootstrapTokenPath()}
{/* Collapsible alternatives */}
# For Kubernetes:
kubectl exec <pod-name> -- cat {bootstrapTokenPath()}
# For Proxmox LXC running Docker:
pct exec <ctid> -- docker exec <container-name> cat {bootstrapTokenPath()}
# Enter container then run:
pct enter {lxcCtid() || ''}
cat {bootstrapTokenPath()}
# Direct file access:
cat {bootstrapTokenPath()}
{/* 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 */}
{/* Username */}
setUsername(e.currentTarget.value)} 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" placeholder="admin" />
{/* Password Setup */}
setPassword(e.currentTarget.value)} 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" placeholder="Enter password (min 12 characters)" /> setConfirmPassword(e.currentTarget.value)} 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" placeholder="Confirm password" />

A secure 16-character password will be generated for you. Make sure to save it when shown!

{/* Theme Selection */}

{themeMode() === 'system' ? 'Using your operating system theme preference' : `Using ${themeMode()} mode`}

{/* Info Box */}
  • Your admin account will be created
  • An API token will be generated for automation
  • All API endpoints will be protected
  • You'll need to login to access the dashboard
{/* Setup Button */}

Save your credentials now - they won't be shown again

{/* Username */}
{savedUsername()}
{/* Password */}
{savedPassword()}
{/* API Token */}
{savedToken()}
{/* Warning */}

⚠️ Important

These credentials will never be shown again. Save them in a password manager now!

{/* Action Buttons */}
); };