diff --git a/apps/pwa/src/routes/SetupPage.tsx b/apps/pwa/src/routes/SetupPage.tsx index 2d310d4..fcb9c5b 100644 --- a/apps/pwa/src/routes/SetupPage.tsx +++ b/apps/pwa/src/routes/SetupPage.tsx @@ -8,8 +8,9 @@ * Step 3: Calendar Credential (Fastmail email + app password; CalDAV validation) * Terminal: "Setup complete" (Surface 7) — replaces wizard card after step 3 completes * - * Step 2 validates DB connectivity (POST /api/setup/validate/db) and OIDC discovery - * (POST /api/setup/validate/oidc) AFTER writing config (POST /api/setup/config). + * Step 2 validates DB connectivity (POST /api/setup/validate/db), OIDC discovery + * (POST /api/setup/validate/oidc), and VAPID key pair (POST /api/setup/validate/vapid) + * AFTER writing config (POST /api/setup/config). All three must pass to proceed. * Step 3 validates CalDAV PROPFIND (POST /api/setup/credential), then calls * POST /api/setup/complete to flip setup_complete. * @@ -26,6 +27,7 @@ import { postSetupConfig, validateSetupDb, validateSetupOidc, + validateSetupVapid, postSetupCredential, postSetupComplete, SetupAlreadyLockedError, @@ -43,6 +45,7 @@ type ValidationRowState = 'idle' | 'pending' | 'success' | 'failure'; interface ValidationRowStatus { db: ValidationRowState; oidc: ValidationRowState; + vapid: ValidationRowState; caldav: ValidationRowState; } @@ -439,9 +442,10 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) { const [vapidPublicKey, setVapidPublicKey] = useState(''); const [fieldError, setFieldError] = useState(null); - const [validationRows, setValidationRows] = useState>({ + const [validationRows, setValidationRows] = useState>({ db: 'idle', oidc: 'idle', + vapid: 'idle', }); // Track overall state: null = not yet run, 'running', 'done' (both pass), 'failed' @@ -453,15 +457,26 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) { onSuccess: async () => { setConfigSaved(true); setFieldError(null); - // Now run DB validation - setValidationRows({ db: 'pending', oidc: 'idle' }); + // Sequential validation: DB → OIDC → VAPID + setValidationRows({ db: 'pending', oidc: 'idle', vapid: 'idle' }); try { await validateSetupDb(); - setValidationRows({ db: 'success', oidc: 'pending' }); + setValidationRows({ db: 'success', oidc: 'pending', vapid: 'idle' }); try { await validateSetupOidc(); - setValidationRows({ db: 'success', oidc: 'success' }); - setBothPassed(true); + setValidationRows({ db: 'success', oidc: 'success', vapid: 'pending' }); + try { + await validateSetupVapid(); + setValidationRows({ db: 'success', oidc: 'success', vapid: 'success' }); + setBothPassed(true); + } catch (vapidErr) { + setValidationRows((prev) => ({ ...prev, vapid: 'failure' })); + setFieldError( + vapidErr instanceof Error + ? vapidErr.message + : 'VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.', + ); + } } catch (oidcErr) { setValidationRows((prev) => ({ ...prev, oidc: 'failure' })); setFieldError( @@ -471,7 +486,7 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) { ); } } catch (dbErr) { - setValidationRows({ db: 'failure', oidc: 'idle' }); + setValidationRows({ db: 'failure', oidc: 'idle', vapid: 'idle' }); setFieldError( dbErr instanceof Error ? dbErr.message @@ -487,14 +502,17 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) { }); const isSaveValidating = configMutation.isPending; - const isValidating = validationRows.db === 'pending' || validationRows.oidc === 'pending'; + const isValidating = + validationRows.db === 'pending' || + validationRows.oidc === 'pending' || + validationRows.vapid === 'pending'; const anyPending = isSaveValidating || isValidating; function handleSaveAndValidate() { setFieldError(null); setBothPassed(false); setConfigSaved(false); - setValidationRows({ db: 'idle', oidc: 'idle' }); + setValidationRows({ db: 'idle', oidc: 'idle', vapid: 'idle' }); if (!appUrl.trim() || !oidcIssuer.trim() || !oidcClientId.trim() || !vapidPublicKey.trim()) { setFieldError('All fields are required.'); @@ -644,9 +662,18 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) { 'OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server.' } /> + {/* General field error (before validation rows show) */} - {fieldError && validationRows.db === 'idle' && validationRows.oidc === 'idle' && ( + {fieldError && validationRows.db === 'idle' && validationRows.oidc === 'idle' && validationRows.vapid === 'idle' && (