fix(setup): record email unverified so onboarding works without SMTP

At bootstrap there is no SMTP, so the old /setup flow was unreachable: it
requested an emailed OTP that could never arrive. Setup now records the
Owner's email address unverified (no OTP round-trip) and requires a passkey,
deferring SMTP configuration to a later Settings page. Setup completes on
email-recorded + passkey-enrolled, and the lockdown lifts on the passkey, not
on email_verified: a passkey is the Owner's only pre-SMTP login credential
(email-OTP login refuses admin accounts).

The record-email endpoint (POST /account/email) now clears email_verified in
the same write. Only VerifyEmailOTP, which proves control of the address, may
set that flag; recording a fresh unproven address must never leave a stale
email_verified=true asserting a proof the user never gave. The change strictly
tightens the invariant, so no existing reader breaks.

Remove the dead ErrEmailTaken path and its documented 409: no migration puts a
unique index on users.email and the codebase does not enforce email
uniqueness, so the unique-violation branch was unreachable and the 409 an
impossible response.

The /setup route (Setup.tsx, setEmail helper, setup i18n copy) is rewritten to
match: record-email, mandatory passkey, no skip-for-now. The SMTP settings
page and post-setup configure-SMTP nudge are deferred.
This commit is contained in:
flyemoji committed 2026-07-17 01:41:07 +09:00
1 parent 93190e7a5b
commit 87279a1366
12 files changed
+328 -155

No files matched your search

+40
View File
@@ -3317,6 +3317,46 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/Error' }
/api/v1/account/email:
post:
tags: [account]
operationId: setEmail
summary: Record the caller's email WITHOUT verifying it (setup bootstrap, spec §B2).
description: >
Writes the supplied address to the authenticated principal's user row and
clears email_verified (already false for a fresh Owner). The setup bootstrap
has no SMTP, so the Owner cannot receive an emailed code; a later Settings/SMTP
flow proves control of the address via /account/email/verify.
x-felis-face: [external]
x-felis-tier: app
security: [{ accessJWT: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email: { type: string, format: email }
responses:
'200':
description: Email recorded (unverified).
content:
application/json:
schema:
type: object
required: [email]
properties:
email: { type: string, format: email }
'400':
description: A valid email is required.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'401':
$ref: '#/components/responses/Unauthorized'
/api/v1/account/passkey/register/begin:
post:
tags: [account]