feat(portal): add the set-password page the setup email links to #459

Merged
Claude-Run merged 1 commit from feat/MAPPS-396-portal-set-password into main 2026-08-02 00:23:58 +02:00
Member

mokosh-server has mailed {CLIENT_ORIGIN}/portal/set-password?token={token} on every portal-access grant since PMS-136 (src/modules/contacts/service.rs), but the SPA declared no such route, so every customer who clicked the link landed on the catch-all 404 and no contact could ever set a portal password. This adds the page behind that URL.

PortalSetPassword sits in the portal block of the Route enum, outside AuthGuard: the emailed single-use token is the only credential the visitor holds, so the page reads it from ?token= via current_query_param and POSTs {token, password} to POST /api/v1/portal/auth/setup-password with no bearer. The token is {contact_id}.{64 alphanumerics} (ContactService::insert_setup_token), so the raw query value needs no decoding. The form takes a password plus confirmation and enforces the 8-character floor client-side, mirroring the length(min = 8) validator on PortalSetupPasswordRequest.

The endpoint answers 204 with an empty body, which post_typed cannot decode, so hooks::fetch::api gains post_typed_no_content: an unauthed POST that only inspects the status and keeps the typed ApiError so the three documented outcomes stay distinguishable. 204 shows the success panel, 410 reads "This link was already used. Your password is set, so sign in to the portal instead.", and 400 reads "This link is expired or invalid. Ask your account team for a new one." (preferring the server's own message when it carries one, since 400 also covers a rejected password).

Success lands on the portal entry point rather than a sign-in form: the /portal/login page is MAPPS-395's deliverable and does not exist yet, so linking there today would send the customer straight back to the 404 this change is fixing. When MAPPS-395 lands, the Route::PortalHome target in the success panel becomes Route::PortalLogin.

The recurrence gate is a unit test, emailed_link_routes::every_emailed_link_resolves_to_a_route in src/lib.rs, which parses every link mokosh-server builds on the SPA origin and asserts none falls through to NotFound. The list is the full set of emitters swept on mokosh-server main at 38c8945, not just the two the issue named: /portal/set-password (contacts), /portal/quotes/{id} (quotes), /reset-password/{token} (auth password reset and staff welcome), and the bare origin (auth security notice, invitations). Verified as a real gate by deleting the new route and watching the test fail with "contacts::send_setup_email emails /portal/set-password, which falls through to the 404 catch-all".

Checks green: cargo fmt --all --check, cargo clippy --all-targets -D warnings, cargo check --target wasm32-unknown-unknown, cargo test --lib (245 passed, 0 failed; was 244).

#MAPPS-396

mokosh-server has mailed `{CLIENT_ORIGIN}/portal/set-password?token={token}` on every portal-access grant since PMS-136 (`src/modules/contacts/service.rs`), but the SPA declared no such route, so every customer who clicked the link landed on the catch-all 404 and no contact could ever set a portal password. This adds the page behind that URL. `PortalSetPassword` sits in the portal block of the `Route` enum, outside `AuthGuard`: the emailed single-use token is the only credential the visitor holds, so the page reads it from `?token=` via `current_query_param` and POSTs `{token, password}` to `POST /api/v1/portal/auth/setup-password` with no bearer. The token is `{contact_id}.{64 alphanumerics}` (`ContactService::insert_setup_token`), so the raw query value needs no decoding. The form takes a password plus confirmation and enforces the 8-character floor client-side, mirroring the `length(min = 8)` validator on `PortalSetupPasswordRequest`. The endpoint answers 204 with an empty body, which `post_typed` cannot decode, so `hooks::fetch::api` gains `post_typed_no_content`: an unauthed POST that only inspects the status and keeps the typed `ApiError` so the three documented outcomes stay distinguishable. 204 shows the success panel, 410 reads "This link was already used. Your password is set, so sign in to the portal instead.", and 400 reads "This link is expired or invalid. Ask your account team for a new one." (preferring the server's own message when it carries one, since 400 also covers a rejected password). Success lands on the portal entry point rather than a sign-in form: the `/portal/login` page is MAPPS-395's deliverable and does not exist yet, so linking there today would send the customer straight back to the 404 this change is fixing. When MAPPS-395 lands, the `Route::PortalHome` target in the success panel becomes `Route::PortalLogin`. The recurrence gate is a unit test, `emailed_link_routes::every_emailed_link_resolves_to_a_route` in src/lib.rs, which parses every link mokosh-server builds on the SPA origin and asserts none falls through to `NotFound`. The list is the full set of emitters swept on mokosh-server main at 38c8945, not just the two the issue named: `/portal/set-password` (contacts), `/portal/quotes/{id}` (quotes), `/reset-password/{token}` (auth password reset and staff welcome), and the bare origin (auth security notice, invitations). Verified as a real gate by deleting the new route and watching the test fail with "contacts::send_setup_email emails /portal/set-password, which falls through to the 404 catch-all". Checks green: cargo fmt --all --check, cargo clippy --all-targets -D warnings, cargo check --target wasm32-unknown-unknown, cargo test --lib (245 passed, 0 failed; was 244). #MAPPS-396
feat(portal): add the set-password page the setup email links to
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 2m45s
Create release / Create release from merged PR (pull_request) Has been skipped
fa1fdfdd7a
mokosh-server has mailed `{CLIENT_ORIGIN}/portal/set-password?token={token}` on every portal-access grant since PMS-136 (`src/modules/contacts/service.rs`), but the SPA declared no such route, so every customer who clicked the link landed on the catch-all 404 and no contact could ever set a portal password. This adds the page behind that URL.

`PortalSetPassword` sits in the portal block of the `Route` enum, outside `AuthGuard`: the emailed single-use token is the only credential the visitor holds, so the page reads it from `?token=` via `current_query_param` and POSTs `{token, password}` to `POST /api/v1/portal/auth/setup-password` with no bearer. The token is `{contact_id}.{64 alphanumerics}` (`ContactService::insert_setup_token`), so the raw query value needs no decoding. The form takes a password plus confirmation and enforces the 8-character floor client-side, mirroring the `length(min = 8)` validator on `PortalSetupPasswordRequest`.

The endpoint answers 204 with an empty body, which `post_typed` cannot decode, so `hooks::fetch::api` gains `post_typed_no_content`: an unauthed POST that only inspects the status and keeps the typed `ApiError` so the three documented outcomes stay distinguishable. 204 shows the success panel, 410 reads "This link was already used. Your password is set, so sign in to the portal instead.", and 400 reads "This link is expired or invalid. Ask your account team for a new one." (preferring the server's own message when it carries one, since 400 also covers a rejected password).

Success lands on the portal entry point rather than a sign-in form: the `/portal/login` page is MAPPS-395's deliverable and does not exist yet, so linking there today would send the customer straight back to the 404 this change is fixing. When MAPPS-395 lands, the `Route::PortalHome` target in the success panel becomes `Route::PortalLogin`.

The recurrence gate is a unit test, `emailed_link_routes::every_emailed_link_resolves_to_a_route` in src/lib.rs, which parses every link mokosh-server builds on the SPA origin and asserts none falls through to `NotFound`. The list is the full set of emitters swept on mokosh-server main at 38c8945, not just the two the issue named: `/portal/set-password` (contacts), `/portal/quotes/{id}` (quotes), `/reset-password/{token}` (auth password reset and staff welcome), and the bare origin (auth security notice, invitations). Verified as a real gate by deleting the new route and watching the test fail with "contacts::send_setup_email emails /portal/set-password, which falls through to the 404 catch-all".

Checks green: cargo fmt --all --check, cargo clippy --all-targets -D warnings, cargo check --target wasm32-unknown-unknown, cargo test --lib (245 passed, 0 failed; was 244).

#MAPPS-396
Claude-Run deleted branch feat/MAPPS-396-portal-set-password 2026-08-02 00:23:58 +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!459
No description provided.