fix(12-04): wire validateSetupVapid into setup wizard (close CR-01 / SETUP-02 gap)
- Import validateSetupVapid from api/client.ts in SetupPage.tsx - Add vapid: ValidationRowState to validationRows state (alongside db/oidc) - Extend configMutation.onSuccess chain: DB → OIDC → VAPID (sequential) - Add ValidationRow for VAPID with pending/success/failure text - Gate setBothPassed(true) on all three rows passing (db AND oidc AND vapid) - Update anyPending and handleSaveAndValidate reset to include vapid state - All 249 PWA tests pass; TypeScript clean Closes CR-01; satisfies SETUP-02 "VAPID private key decodes to 32 bytes" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
7d0205df05
commit
0d53249b02
@@ -8,8 +8,9 @@
|
|||||||
* Step 3: Calendar Credential (Fastmail email + app password; CalDAV validation)
|
* Step 3: Calendar Credential (Fastmail email + app password; CalDAV validation)
|
||||||
* Terminal: "Setup complete" (Surface 7) — replaces wizard card after step 3 completes
|
* 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
|
* Step 2 validates DB connectivity (POST /api/setup/validate/db), OIDC discovery
|
||||||
* (POST /api/setup/validate/oidc) AFTER writing config (POST /api/setup/config).
|
* (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
|
* Step 3 validates CalDAV PROPFIND (POST /api/setup/credential), then calls
|
||||||
* POST /api/setup/complete to flip setup_complete.
|
* POST /api/setup/complete to flip setup_complete.
|
||||||
*
|
*
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
postSetupConfig,
|
postSetupConfig,
|
||||||
validateSetupDb,
|
validateSetupDb,
|
||||||
validateSetupOidc,
|
validateSetupOidc,
|
||||||
|
validateSetupVapid,
|
||||||
postSetupCredential,
|
postSetupCredential,
|
||||||
postSetupComplete,
|
postSetupComplete,
|
||||||
SetupAlreadyLockedError,
|
SetupAlreadyLockedError,
|
||||||
@@ -43,6 +45,7 @@ type ValidationRowState = 'idle' | 'pending' | 'success' | 'failure';
|
|||||||
interface ValidationRowStatus {
|
interface ValidationRowStatus {
|
||||||
db: ValidationRowState;
|
db: ValidationRowState;
|
||||||
oidc: ValidationRowState;
|
oidc: ValidationRowState;
|
||||||
|
vapid: ValidationRowState;
|
||||||
caldav: ValidationRowState;
|
caldav: ValidationRowState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,9 +442,10 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
|
|||||||
const [vapidPublicKey, setVapidPublicKey] = useState('');
|
const [vapidPublicKey, setVapidPublicKey] = useState('');
|
||||||
const [fieldError, setFieldError] = useState<string | null>(null);
|
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',
|
db: 'idle',
|
||||||
oidc: 'idle',
|
oidc: 'idle',
|
||||||
|
vapid: 'idle',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track overall state: null = not yet run, 'running', 'done' (both pass), 'failed'
|
// Track overall state: null = not yet run, 'running', 'done' (both pass), 'failed'
|
||||||
@@ -453,15 +457,26 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setConfigSaved(true);
|
setConfigSaved(true);
|
||||||
setFieldError(null);
|
setFieldError(null);
|
||||||
// Now run DB validation
|
// Sequential validation: DB → OIDC → VAPID
|
||||||
setValidationRows({ db: 'pending', oidc: 'idle' });
|
setValidationRows({ db: 'pending', oidc: 'idle', vapid: 'idle' });
|
||||||
try {
|
try {
|
||||||
await validateSetupDb();
|
await validateSetupDb();
|
||||||
setValidationRows({ db: 'success', oidc: 'pending' });
|
setValidationRows({ db: 'success', oidc: 'pending', vapid: 'idle' });
|
||||||
try {
|
try {
|
||||||
await validateSetupOidc();
|
await validateSetupOidc();
|
||||||
setValidationRows({ db: 'success', oidc: 'success' });
|
setValidationRows({ db: 'success', oidc: 'success', vapid: 'pending' });
|
||||||
setBothPassed(true);
|
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) {
|
} catch (oidcErr) {
|
||||||
setValidationRows((prev) => ({ ...prev, oidc: 'failure' }));
|
setValidationRows((prev) => ({ ...prev, oidc: 'failure' }));
|
||||||
setFieldError(
|
setFieldError(
|
||||||
@@ -471,7 +486,7 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (dbErr) {
|
} catch (dbErr) {
|
||||||
setValidationRows({ db: 'failure', oidc: 'idle' });
|
setValidationRows({ db: 'failure', oidc: 'idle', vapid: 'idle' });
|
||||||
setFieldError(
|
setFieldError(
|
||||||
dbErr instanceof Error
|
dbErr instanceof Error
|
||||||
? dbErr.message
|
? dbErr.message
|
||||||
@@ -487,14 +502,17 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isSaveValidating = configMutation.isPending;
|
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;
|
const anyPending = isSaveValidating || isValidating;
|
||||||
|
|
||||||
function handleSaveAndValidate() {
|
function handleSaveAndValidate() {
|
||||||
setFieldError(null);
|
setFieldError(null);
|
||||||
setBothPassed(false);
|
setBothPassed(false);
|
||||||
setConfigSaved(false);
|
setConfigSaved(false);
|
||||||
setValidationRows({ db: 'idle', oidc: 'idle' });
|
setValidationRows({ db: 'idle', oidc: 'idle', vapid: 'idle' });
|
||||||
|
|
||||||
if (!appUrl.trim() || !oidcIssuer.trim() || !oidcClientId.trim() || !vapidPublicKey.trim()) {
|
if (!appUrl.trim() || !oidcIssuer.trim() || !oidcClientId.trim() || !vapidPublicKey.trim()) {
|
||||||
setFieldError('All fields are required.');
|
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.'
|
'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) */}
|
{/* 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
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
|
|||||||
Reference in New Issue
Block a user