feat(web): forced post-registration onboarding (BUNYIP-206) #228

Merged
nrupard merged 1 commit from feat/bunyip-206-onboarding-page into main 2026-06-26 00:01:15 +02:00
Owner

What

Forced post-registration onboarding (BUNYIP-206). A new user lands on /onboarding and cannot reach the dashboard or any app surface until they have provided a first + last name and (when email delivery is configured) verified their email. Replaces the passive BUNYIP-139 dashboard name banner with a hard gate at the single bunyip-web chokepoint.

How

  • Gate (handlers/mod.rs): needs_onboarding(st, user) blocks while the name is missing, or - only when setup_status.email_enabled is true - while the email is unverified. Applied in guard() after the admin 2FA-setup gate (which keeps precedence and is let through on its own page so the two gates never fight) and skipped for an allowlist (/onboarding, /settings/verify-email, /settings/verify-email/resend, /logout, /assets). Bootstrap admins (already named + verified) skip it entirely.
  • Page (handlers/onboarding.rs, new; routes in main.rs): GET/POST /onboarding in the authenticated shell. Name step saves via the existing PUT /v1/users/me/profile; email step shows verification state + a Resend control. Auto-sends the verification email on first arrival only, cookie-gated by bunyip_onboard_verify_sent.
  • Supporting: register_post -> /onboarding; the emailed verification-link success card forwards to /dashboard (the gate routes a still-nameless user back to /onboarding) instead of dumping the user on settings; settings_resend_verification guards with its real route path so the allowlist admits it; the dashboard name banner + profile_empty removed.

Two root-cause decisions worth calling out

  1. Email requirement is conditional on delivery being enabled. The issue assumed email_verified is always required. But email.enabled = (is_production && has_smtp) || EMAIL_ENABLED (config.rs), so email is OFF in local dev by default. An unconditional email-verification gate would permanently trap any dev-registered user (no email -> can never verify -> stuck on /onboarding forever). The gate therefore treats email verification as not-required when delivery is off. On prod (email on) the issue's AC holds exactly. setup_status is only queried for the rare name-present-but-unverified case, so the common already-onboarded path adds no API call.
  2. Auto-send is cookie-gated, not per-load. The issue called request_email_verification "idempotent / re-send safe". It is not: it mints a fresh token and sends a new mail on every call, capped at 3/hour (services/auth.rs). A literal send-on-every-GET would spam the inbox and wall at the rate limit (the POST-name -> redirect -> GET cycle alone would double-send). The cookie (bunyip_onboard_verify_sent, 1h) makes it fire once per arrival; the send result is swallowed so a rate-limit or disabled-email never breaks the page.

Migration note

Existing users who never verified their email (or have no name) will be routed through /onboarding on their next protected request. That is the intended "forced" behaviour, but it is a visible change for that cohort.

Test

just check-container green (fmt + clippy -D warnings + cargo test --workspace --lib). New unit tests cover names_present (both-names / whitespace) and the onboarding_allowed allowlist. The email-driven leg is exercised by the e2e suite on staging (mail sink, BUNYIP-150); the e2e auth/signup spec remains test.fixme (BUNYIP-150) and will assert the /onboarding landing when it is un-fixmed.

Docs

KB DEV-A-40 (Login / Authentication system) updated: the onboarding flow, gate ordering, the email-disabled exemption, and the non-idempotent-verify gotcha.

🤖 Generated with Claude Code

## What Forced post-registration onboarding (BUNYIP-206). A new user lands on `/onboarding` and cannot reach the dashboard or any app surface until they have provided a first + last name and (when email delivery is configured) verified their email. Replaces the passive BUNYIP-139 dashboard name banner with a hard gate at the single bunyip-web chokepoint. ## How - **Gate** (`handlers/mod.rs`): `needs_onboarding(st, user)` blocks while the name is missing, or - only when `setup_status.email_enabled` is true - while the email is unverified. Applied in `guard()` after the admin 2FA-setup gate (which keeps precedence and is let through on its own page so the two gates never fight) and skipped for an allowlist (`/onboarding`, `/settings/verify-email`, `/settings/verify-email/resend`, `/logout`, `/assets`). Bootstrap admins (already named + verified) skip it entirely. - **Page** (`handlers/onboarding.rs`, new; routes in `main.rs`): `GET/POST /onboarding` in the authenticated shell. Name step saves via the existing `PUT /v1/users/me/profile`; email step shows verification state + a Resend control. Auto-sends the verification email on first arrival only, cookie-gated by `bunyip_onboard_verify_sent`. - **Supporting**: `register_post` -> `/onboarding`; the emailed verification-link success card forwards to `/dashboard` (the gate routes a still-nameless user back to `/onboarding`) instead of dumping the user on settings; `settings_resend_verification` guards with its real route path so the allowlist admits it; the dashboard name banner + `profile_empty` removed. ## Two root-cause decisions worth calling out 1. **Email requirement is conditional on delivery being enabled.** The issue assumed `email_verified` is always required. But `email.enabled = (is_production && has_smtp) || EMAIL_ENABLED` (`config.rs`), so email is OFF in local dev by default. An unconditional email-verification gate would permanently trap any dev-registered user (no email -> can never verify -> stuck on `/onboarding` forever). The gate therefore treats email verification as not-required when delivery is off. On prod (email on) the issue's AC holds exactly. `setup_status` is only queried for the rare name-present-but-unverified case, so the common already-onboarded path adds no API call. 2. **Auto-send is cookie-gated, not per-load.** The issue called `request_email_verification` "idempotent / re-send safe". It is not: it mints a fresh token and sends a new mail on every call, capped at 3/hour (`services/auth.rs`). A literal send-on-every-GET would spam the inbox and wall at the rate limit (the POST-name -> redirect -> GET cycle alone would double-send). The cookie (`bunyip_onboard_verify_sent`, 1h) makes it fire once per arrival; the send result is swallowed so a rate-limit or disabled-email never breaks the page. ## Migration note Existing users who never verified their email (or have no name) will be routed through `/onboarding` on their next protected request. That is the intended "forced" behaviour, but it is a visible change for that cohort. ## Test `just check-container` green (fmt + clippy `-D warnings` + `cargo test --workspace --lib`). New unit tests cover `names_present` (both-names / whitespace) and the `onboarding_allowed` allowlist. The email-driven leg is exercised by the e2e suite on staging (mail sink, BUNYIP-150); the e2e `auth/signup` spec remains `test.fixme` (BUNYIP-150) and will assert the `/onboarding` landing when it is un-fixmed. ## Docs KB **DEV-A-40** (Login / Authentication system) updated: the onboarding flow, gate ordering, the email-disabled exemption, and the non-idempotent-verify gotcha. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): forced post-registration onboarding (name + email verification)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 20s
Check / fmt + clippy + build + tests (pull_request) Successful in 2m12s
Create release / Create release from merged PR (pull_request) Has been skipped
ebb001d1f5
A freshly registered user now lands on /onboarding and cannot reach the dashboard or any app surface until they have provided a first and last name and (when email delivery is configured) verified their email. This replaces the passive BUNYIP-139 dashboard name banner with a hard gate at the single bunyip-web chokepoint.

Gate: `needs_onboarding(st, user)` (handlers/mod.rs) blocks while the name is missing, or - only when `setup_status.email_enabled` is true - while the email is unverified. The email requirement is intentionally relaxed when delivery is OFF (local dev / no-SMTP deploys, where `email.enabled = (is_production && has_smtp) || EMAIL_ENABLED`) so the gate can never permanently trap a user who could never receive the verification link; `setup_status` is only queried for the rare name-present-but-unverified case, so the common already-onboarded path stays a no-op with no extra API call. `guard()` applies the gate after the admin 2FA-setup gate (which keeps precedence and is let through on its own page so the two gates never fight) and skips it for an allowlist (/onboarding, /settings/verify-email, /settings/verify-email/resend, /logout, /assets). Bootstrap admins (already named + verified) skip it entirely.

Page: GET/POST /onboarding (new handlers::onboarding module, registered in main.rs) renders in the authenticated shell. The name step saves through the existing `PUT /v1/users/me/profile`; the email step shows verification state and a Resend control. On first arrival only it auto-sends the verification email, cookie-gated by `bunyip_onboard_verify_sent` because `request_email_verification` is NOT idempotent (mints a fresh token + sends a new mail per call, capped at 3/hour) - so a naive send-on-every-load would spam the inbox and wall at the rate limit. The send result is swallowed so a rate-limit or disabled-email never breaks the page.

Supporting changes: register_post redirects to /onboarding (the gate would route there anyway, this avoids the extra hop); the emailed verification link's success card now forwards to /dashboard (the gate routes a still-nameless user back to /onboarding) instead of dumping the user on settings; settings_resend_verification now guards with its real route path so the onboarding allowlist admits it; the dashboard name banner and its `profile_empty` computation are removed.

KB DEV-A-40 updated (flow, gate ordering, the email-disabled exemption, and the non-idempotent-verify gotcha).

#BUNYIP-206

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard scheduled this pull request to auto merge when all checks succeed 2026-06-25 23:57:46 +02:00
nrupard deleted branch feat/bunyip-206-onboarding-page 2026-06-26 00:01:15 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!228
No description provided.