feat(setup): add the op.console owner onboarding wizard

The /setup route redeems the one-time token from `felis setup`, then walks
the Owner through email-OTP verification and passkey enrollment before
handing off to the console. It sits outside RequireAuth — the visitor
arrives without a session and the redeem is what mints one — and is
reload-safe: a spent token resumes from the surviving session via
/auth/setup/status.

Adds the Setup page and its /setup route, the setup API client methods
(redeem/status), and the en-US/zh-CN onboarding strings.
This commit is contained in:
flyemoji committed 2026-07-16 18:03:58 +09:00
1 parent 60732a6283
commit 5fbb1db4ec
5 files changed
+450 -2

No files matched your search

+22
View File
@@ -94,6 +94,19 @@ async function requestRaw<T>(
return parsed as T;
}
// Setup bootstrap (spec §B). The one-time token from `felis setup` is redeemed for
// a lockdown session; the response (and /setup/status) reports which onboarding
// steps remain so the Setup wizard can drive email verification + passkey enrollment.
export interface SetupState {
user_id: string;
username: string;
role: string;
email: string | null;
email_verified: boolean;
has_passkey: boolean;
setup_required: boolean;
}
export const api = {
// Local-password auth (spec §B1). login sets an HttpOnly session cookie as a
// side effect — the panel never sees it — and returns only what to route on next
@@ -139,6 +152,15 @@ export const api = {
new_password,
}),
// Setup bootstrap (spec §B). redeem consumes the one-time token from the setup URL
// and mints a lockdown session (Public); status re-reads progress for a reload
// mid-wizard (SetupAllowed — the surviving session, no token needed).
setupRedeem: (token: string) =>
request<SetupState>("POST", "/auth/setup/redeem", { token }),
setupStatus: () =>
request<SetupState>("GET", "/auth/setup/status"),
// Identity (spec §7 GET /me) — the tier keystone. is_admin is server-computed
// (Principal.IsAdmin); the panel reads it but re-deriving admin-ness is the
// backend's job. Drives nav + route guards only; every admin route 403s on its