feat(auth): forced name-onboarding gate for new Bunyip-JIT users #106

Merged
YousifShkara merged 1 commit from feat/force-name-onboarding into main 2026-06-10 05:19:29 +02:00
Owner

mokosh-server JIT-creates Bunyip OIDC users with a synthetic name derived from the email local-part (the at+jwt carries no name claims). Until now the user landed on the dashboard with "User abc12" in the topbar and had to discover the Profile page on their own to fix it. New users are now redirected to a forced onboarding screen until they confirm first + last name; everything else (timezone, preferences, etc.) stays optional and lives on the existing Profile page.

The server side ships profile_completed: bool on GET /api/v1/auth/me (paired patch: mokosh-server feat/force-profile-setup-on-jit). This SPA reads the flag, refuses to render anything else, and only releases the gate when the next /me reports true.

Mechanics:

  • CurrentUser gains profile_completed: bool with #[serde(default = "default_true")] so legacy server responses (no field) don't surprise-trap existing users.
  • refresh_user_from_me (the background /me reconciler called after every token refresh and on memberships load) pulls the new field and writes it back to the in-memory CurrentUser. Auth callback + sessionStorage rehydration default to true; the first /me reconciles within one tick.
  • New route /onboarding/profile under the AuthGuard layout but EXEMPTED from its own redirect via a pathname check (web_sys::window().location().pathname() so we read it synchronously inside render, no signal re-entry).
  • AuthGuard render-time redirect: if the user is authenticated AND profile_completed = false AND the current pathname is not /onboarding/profile, nav.replace(Route::Onboarding {}). No frame of authenticated UI ever paints for an un-onboarded user.
  • pages/onboarding.rs is a single-purpose page: full-screen centred card, two required Inputs (first/last name), Continue button. Submit calls PUT /api/v1/auth/me via the existing put_authed_typed helper, reflects the response back into the in-memory CurrentUser, then nav.replace(Dashboard). Defence-in-depth: if a completed user hits the URL directly, a use_effect bounces them to the dashboard.

Server's update_user handler appends profile_completed_at = COALESCE(profile_completed_at, NOW()) whenever the PUT body has non-empty first + last name (matching server patch), so the SAME PUT /me call the existing Profile page uses also marks onboarding complete - no separate endpoint.

Backwards compat for the existing user base is handled by the server-side migration backfilling all current rows to profile_completed_at = NOW(); the SPA just inherits the resulting true.

mokosh-server JIT-creates Bunyip OIDC users with a synthetic name derived from the email local-part (the at+jwt carries no name claims). Until now the user landed on the dashboard with "User abc12" in the topbar and had to discover the Profile page on their own to fix it. New users are now redirected to a forced onboarding screen until they confirm first + last name; everything else (timezone, preferences, etc.) stays optional and lives on the existing Profile page. The server side ships `profile_completed: bool` on `GET /api/v1/auth/me` (paired patch: mokosh-server `feat/force-profile-setup-on-jit`). This SPA reads the flag, refuses to render anything else, and only releases the gate when the next `/me` reports `true`. Mechanics: - `CurrentUser` gains `profile_completed: bool` with `#[serde(default = "default_true")]` so legacy server responses (no field) don't surprise-trap existing users. - `refresh_user_from_me` (the background `/me` reconciler called after every token refresh and on memberships load) pulls the new field and writes it back to the in-memory `CurrentUser`. Auth callback + sessionStorage rehydration default to `true`; the first `/me` reconciles within one tick. - New route `/onboarding/profile` under the `AuthGuard` layout but EXEMPTED from its own redirect via a pathname check (web_sys::window().location().pathname() so we read it synchronously inside render, no signal re-entry). - `AuthGuard` render-time redirect: if the user is authenticated AND `profile_completed = false` AND the current pathname is not `/onboarding/profile`, `nav.replace(Route::Onboarding {})`. No frame of authenticated UI ever paints for an un-onboarded user. - `pages/onboarding.rs` is a single-purpose page: full-screen centred card, two required Inputs (first/last name), Continue button. Submit calls `PUT /api/v1/auth/me` via the existing `put_authed_typed` helper, reflects the response back into the in-memory `CurrentUser`, then `nav.replace(Dashboard)`. Defence-in-depth: if a completed user hits the URL directly, a `use_effect` bounces them to the dashboard. Server's `update_user` handler appends `profile_completed_at = COALESCE(profile_completed_at, NOW())` whenever the PUT body has non-empty first + last name (matching server patch), so the SAME `PUT /me` call the existing Profile page uses also marks onboarding complete - no separate endpoint. Backwards compat for the existing user base is handled by the server-side migration backfilling all current rows to `profile_completed_at = NOW()`; the SPA just inherits the resulting `true`.
feat(auth): forced name-onboarding gate for new Bunyip-JIT users
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 49s
d33627f333
mokosh-server JIT-creates Bunyip OIDC users with a synthetic name derived from the email local-part (the at+jwt carries no name claims). Until now the user landed on the dashboard with "User abc12" in the topbar and had to discover the Profile page on their own to fix it. New users are now redirected to a forced onboarding screen until they confirm first + last name; everything else (timezone, preferences, etc.) stays optional and lives on the existing Profile page.

The server side ships `profile_completed: bool` on `GET /api/v1/auth/me` (paired patch: mokosh-server `feat/force-profile-setup-on-jit`). This SPA reads the flag, refuses to render anything else, and only releases the gate when the next `/me` reports `true`.

Mechanics:

- `CurrentUser` gains `profile_completed: bool` with `#[serde(default = "default_true")]` so legacy server responses (no field) don't surprise-trap existing users.
- `refresh_user_from_me` (the background `/me` reconciler called after every token refresh and on memberships load) pulls the new field and writes it back to the in-memory `CurrentUser`. Auth callback + sessionStorage rehydration default to `true`; the first `/me` reconciles within one tick.
- New route `/onboarding/profile` under the `AuthGuard` layout but EXEMPTED from its own redirect via a pathname check (web_sys::window().location().pathname() so we read it synchronously inside render, no signal re-entry).
- `AuthGuard` render-time redirect: if the user is authenticated AND `profile_completed = false` AND the current pathname is not `/onboarding/profile`, `nav.replace(Route::Onboarding {})`. No frame of authenticated UI ever paints for an un-onboarded user.
- `pages/onboarding.rs` is a single-purpose page: full-screen centred card, two required Inputs (first/last name), Continue button. Submit calls `PUT /api/v1/auth/me` via the existing `put_authed_typed` helper, reflects the response back into the in-memory `CurrentUser`, then `nav.replace(Dashboard)`. Defence-in-depth: if a completed user hits the URL directly, a `use_effect` bounces them to the dashboard.

Server's `update_user` handler appends `profile_completed_at = COALESCE(profile_completed_at, NOW())` whenever the PUT body has non-empty first + last name (matching server patch), so the SAME `PUT /me` call the existing Profile page uses also marks onboarding complete - no separate endpoint.

Backwards compat for the existing user base is handled by the server-side migration backfilling all current rows to `profile_completed_at = NOW()`; the SPA just inherits the resulting `true`.
YousifShkara force-pushed feat/force-name-onboarding from d33627f333
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 49s
to e45216aaad
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 45s
2026-06-10 05:18:59 +02:00
Compare
YousifShkara deleted branch feat/force-name-onboarding 2026-06-10 05:19:29 +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/mokosh-apps!106
No description provided.