Phase 12: Initial Setup Wizard #22
@@ -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' });
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user