Skip to content

Commit faf9b94

Browse files
committed
Fix first-run setup 401 error by adding bootstrap token unlock screen (related to #639)
After the security hardening that introduced bootstrap token protection, the first-run setup flow was broken because FirstRunSetup.tsx didn't prompt users for the token. This caused a 401 "Bootstrap setup token required" error during initial admin account creation. Changes: - Add dedicated unlock screen before the setup wizard - Display instructions for retrieving token from host - Include bootstrap token in quick-setup API request headers and body - Only require unlock for first-run setup (skip in force mode) The unlock screen follows the documented flow in README.md and ensures only users with host access can configure an unconfigured instance. Related to #639
1 parent 731eb58 commit faf9b94

File tree

1 file changed

+93
-2
lines changed

1 file changed

+93
-2
lines changed

frontend-modern/src/components/FirstRunSetup.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool
2222
const [savedToken, setSavedToken] = createSignal('');
2323
const [copied, setCopied] = createSignal<'password' | 'token' | null>(null);
2424
const [themeMode, setThemeMode] = createSignal<'system' | 'light' | 'dark'>('system');
25+
const [bootstrapToken, setBootstrapToken] = createSignal('');
26+
const [isUnlocking, setIsUnlocking] = createSignal(false);
27+
const [isUnlocked, setIsUnlocked] = createSignal(false);
2528

2629
const applyTheme = (mode: 'system' | 'light' | 'dark') => {
2730
if (mode === 'light') {
@@ -77,6 +80,15 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool
7780
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
7881
};
7982

83+
const handleUnlock = () => {
84+
if (!bootstrapToken().trim()) {
85+
showError('Please enter the bootstrap token');
86+
return;
87+
}
88+
// Simple client-side unlock - actual validation happens during setup
89+
setIsUnlocked(true);
90+
};
91+
8092
const handleSetup = async () => {
8193
// Validate custom password if used
8294
if (useCustomPassword()) {
@@ -104,15 +116,23 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool
104116
setApiClientToken(token);
105117

106118
try {
119+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
120+
121+
// Include bootstrap token if we're in first-run setup (not force mode)
122+
if (!props.force && bootstrapToken()) {
123+
headers['X-Setup-Token'] = bootstrapToken().trim();
124+
}
125+
107126
const response = await fetch('/api/security/quick-setup', {
108127
method: 'POST',
109-
headers: { 'Content-Type': 'application/json' },
128+
headers,
110129
credentials: 'include', // Include cookies for CSRF
111130
body: JSON.stringify({
112131
username: username(),
113132
password: finalPassword,
114133
apiToken: token,
115134
force: props.force ?? false,
135+
setupToken: bootstrapToken().trim(), // Also include in body as fallback
116136
}),
117137
});
118138

@@ -251,7 +271,78 @@ IMPORTANT: Keep these credentials secure!
251271
</div>
252272

253273
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl overflow-hidden">
254-
<Show when={!showCredentials()}>
274+
{/* Bootstrap Token Unlock Screen */}
275+
<Show when={!isUnlocked() && !showCredentials() && !props.force}>
276+
<div class="p-8">
277+
<SectionHeader
278+
title="Unlock Setup Wizard"
279+
size="lg"
280+
class="mb-6"
281+
titleClass="text-gray-800 dark:text-gray-100"
282+
/>
283+
284+
<div class="space-y-6">
285+
{/* Instructions */}
286+
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
287+
<p class="text-sm text-blue-900 dark:text-blue-100 font-medium mb-2">
288+
To begin setup, retrieve the bootstrap token from your Pulse host:
289+
</p>
290+
<div class="space-y-2">
291+
<div class="bg-white dark:bg-gray-800 rounded p-3 font-mono text-xs text-gray-800 dark:text-gray-200">
292+
<div class="text-blue-600 dark:text-blue-400 mb-1"># Standard installation:</div>
293+
cat /etc/pulse/.bootstrap_token
294+
</div>
295+
<div class="bg-white dark:bg-gray-800 rounded p-3 font-mono text-xs text-gray-800 dark:text-gray-200">
296+
<div class="text-blue-600 dark:text-blue-400 mb-1"># Docker/Helm:</div>
297+
cat /data/.bootstrap_token
298+
</div>
299+
</div>
300+
</div>
301+
302+
{/* Token Input */}
303+
<div>
304+
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
305+
Bootstrap Token
306+
</label>
307+
<input
308+
type="text"
309+
value={bootstrapToken()}
310+
onInput={(e) => setBootstrapToken(e.currentTarget.value)}
311+
onKeyPress={(e) => e.key === 'Enter' && handleUnlock()}
312+
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"
313+
placeholder="Paste the token from your host"
314+
autofocus
315+
/>
316+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
317+
This one-time token ensures only someone with host access can configure Pulse
318+
</p>
319+
</div>
320+
321+
{/* Security Note */}
322+
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
323+
<p class="text-sm text-gray-600 dark:text-gray-400">
324+
<span class="font-semibold text-gray-800 dark:text-gray-200">Why this step?</span>
325+
<br />
326+
The bootstrap token prevents unauthorized access to your unconfigured Pulse instance.
327+
It's automatically removed after you complete the setup wizard.
328+
</p>
329+
</div>
330+
331+
{/* Unlock Button */}
332+
<button
333+
type="button"
334+
onClick={handleUnlock}
335+
disabled={isUnlocking() || !bootstrapToken().trim()}
336+
class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors disabled:cursor-not-allowed"
337+
>
338+
{isUnlocking() ? 'Unlocking...' : 'Unlock Wizard'}
339+
</button>
340+
</div>
341+
</div>
342+
</Show>
343+
344+
{/* Setup Form - only shown after unlock or in force mode */}
345+
<Show when={(isUnlocked() || props.force) && !showCredentials()}>
255346
<div class="p-8">
256347
<SectionHeader
257348
title="Initial security setup"

0 commit comments

Comments
 (0)