Phase 12: Initial Setup Wizard #22

Merged
luckberg merged 76 commits from gsd/phase-12-initial-setup-wizard into main 2026-06-16 19:10:33 -04:00
Showing only changes of commit 0d53249b02 - Show all commits
+39 -12
View File
@@ -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<string | null>(null);
const [validationRows, setValidationRows] = useState<Pick<ValidationRowStatus, 'db' | 'oidc'>>({
const [validationRows, setValidationRows] = useState<Pick<ValidationRowStatus, 'db' | 'oidc' | 'vapid'>>({
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.'
}
/>
<ValidationRow
state={validationRows.vapid}
pendingText="Validating VAPID key pair…"
successText="VAPID keys verified."
failureText={
fieldError ??
'VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.'
}
/>
{/* General field error (before validation rows show) */}
{fieldError && validationRows.db === 'idle' && validationRows.oidc === 'idle' && (
{fieldError && validationRows.db === 'idle' && validationRows.oidc === 'idle' && validationRows.vapid === 'idle' && (
<div
role="status"
aria-live="polite"