13 KiB
Flow — account-and-settings
Last verified: 2026-08-02 against commit
c841bde
Actor(s): every signed-in actor (customer · nurse · admin · partner) · Status: partial Client: partial · Server: real Business source: product/business/01-actors-and-onboarding.md (§(a) as-built auth/session rules; the account-hub UI itself is a UI-phase-9 decision with no product-doc source — see the coverage note in the business-area map) Integration: docs/integration/domains/profiles.md · docs/integration/domains/auth.md
What it does
Each of the four shells ends in a settings hub: who you are signed in as, the handful of preferences the app
keeps (appearance, language, and — for a customer — name and emergency contact), the links out to
notifications and support, and the way out of the app. Since UI phase 9 the customer's /fa/profile is an
account hub, not a profile form: each section opens its own bottom sheet. Sign-out is the one action here
that reaches the server, and it revokes the session rather than just clearing cookies.
Screens
| Step | Route | Component / notes |
|---|---|---|
| Customer hub | /fa/profile |
(customer)/profile/page.tsx — ProfileSummary + rows; three FormDialogShell sheets (اطلاعات شخصی / زبان / مخاطب اضطراری) over one react-hook-form, because the wire upsert has no PATCH semantics (:76-89) |
| Nurse hub | /fa/nurse/more |
NurseMoreScreen.tsx — ProfileSummary (+ TrustBadge), support/notification hub rows, SettingsPanel, SignOutRow |
| Admin hub | /fa/admin/system |
admin/system/page.tsx — the 6 system consoles (capability-filtered) plus identity/appearance/sign-out. Always shown: it is the only way out |
| Partner hub | /fa/partner/more |
partner/more/page.tsx — center name + MoR chip, SettingsPanel, SignOutRow |
| Appearance + language | (in every hub) | SettingsPanel.tsx → ThemeModeSetting (روشن/تیره/سیستم) + LocaleSwitcher. The customer hub does not use SettingsPanel — it inlines ThemeModeSetting (page.tsx:155) and puts LocaleSwitcher inside the زبان sheet (:207) |
| Actor switch | customer + nurse hubs | ActorSwitcher — renders nothing unless the session holds both customer and nurse (:27). No seeded demo account is dual-role |
API
| Call | Endpoint | Notes |
|---|---|---|
| identity for every hub | GET /api/v1/me |
MeController.cs:23. The customer's first/last name is sourced from here by design |
| customer profile read | GET /api/v1/customer_profiles/me |
404 → null (an empty form), not an error — profiles/apis/clientApi.ts:19-26 |
| customer save (all 3 sheets) | POST /api/v1/customer_profiles/upsert |
one call per sheet, always sending the whole profile — clientApi.ts:60-75. Names are written to the users row, not the profile (UpsertCustomerProfileCommand.Handler.cs:45-57) |
| nurse identity | GET /api/v1/nurse_profiles/me |
supplies avatarUrl + verification state for the header |
| partner identity | useMyPartnerCenter() |
mocked — USE_PARTNER_MOCK = true, and the mock resolves MOCK_MY_CENTER_ID = 1 for any caller |
| sign out | POST /api/v1/auth/logout |
AuthController.cs:48 → LogoutCommand.Handler.cs |
Request/response shapes: profiles.md, auth.md.
Does logout actually revoke server-side? Yes — and it revokes everywhere.
Traced: SignOutRow.tsx:14 / profile/page.tsx:269 → useLogout() → authApi.logout({}) →
authClientApi.logout (auth/apis/clientApi.ts:47-52) → POST /api/v1/auth/logout →
LogoutCommand.Handler.cs:25-43.
Both callers send an empty body, and the handler treats a missing refreshToken as
Everywhere: RevokeAllActiveForUserAsync(...) for every active session, then
UpdateSecurityStampAsync, which makes every outstanding JWE access token fail the bearer handler's
stamp check. Cookies are cleared in onSettled regardless of the call's outcome
(useLogout.ts — clearAuthTokens()), so an offline sign-out still ends the local session.
Proven server-side by the integration test RefreshAndLogoutTests.cs:41-53, which posts {} — the exact
client body — and asserts the follow-up /me returns 401. Not probed live on purpose: calling
/auth/logout would revoke the shared pre-minted demo token for that account and break other testers.
Rules that must hold
| Rule | Value / source |
|---|---|
| Sign-out kills all sessions, not just this device | LogoutCommand.cs:12 — no refreshToken ⇒ Everywhere. Session TTL itself is CONFIG auth_session_ttl_days = 30 |
Self-selectable roles are customer and nurse only |
RoleNames.SelfAssignable; an admin sub-role self-assign is a 403. The hub has no role-change affordance at all |
Phones are masked on /me-style payloads |
INV-21 — live /me returns 0912*****10; ProfileSummary renders it as a dir="ltr" island |
| PII is encrypted at rest | INV-21 — the emergency contact name/phone are field-encrypted; only the masked/decrypted read comes back |
| Colors come from tokens, never a literal | docs/rules/client/theme.md; the no-flash boot is CSS-only (client hard rule 9) |
prefers-reduced-motion has exactly one gate |
src/app/globals.css (client hard rule 10) |
Theme mechanics (no JS boot script). ColorSchemeCookieSync (theme/ThemeProvider.tsx:20-30) writes the
resolved scheme to a color-scheme cookie; the root layout reads it (getThemeMode,
lib/cookies/server.ts:42-50) and stamps data-mui-color-scheme on <html> server-side
([locale]/layout.tsx:112,125). With no cookie the attribute is omitted and tokens.css's
@media (prefers-color-scheme: dark) fallback paints instead. ThemeModeSetting is the only subtree
subscribed to useColorScheme().
How to test
- Log in as 09120000010 (سارا محمدی, customer) — see testing-setup.md.
- Open
/fa/profile. Expect: initials avatar, «سارا محمدی», masked0912*****10, a «تماس اضطراری» card with a green check icon reading «بهرام محمدی · 09121110010», and rows for اطلاعات شخصی / نشانیها / زبان / نمایش / اعلانها / پشتیبانی, then a red «خروج از حساب» row. - Tap the نمایش segments روشن → تیره. Expect: the whole app repaints immediately, no reload; reload
the page and the choice survives (the
color-schemecookie), with no flash of the wrong scheme. - Tap زبان → the globe button. Expect: the URL becomes
/en/profile— the same page, not home. The «زبان برنامه» select below it savespreferredLanguageto the server but changes nothing visible (see gaps). - Tap اطلاعات شخصی, change the family name, save. Expect: a «ذخیره شد» toast and the header name
updates (the mutation invalidates
/me). - Tap خروج از حساب → confirm «خروج از حساب؟». Expect: you land on
/fa/login. Re-using that account's old bearer token againstGET /api/v1/menow returns401. - Log in as 09120000001 (زهرا عزیزی, nurse) and open
/fa/nurse/more. Expect: name + a green ✓ تاییدشدهTrustBadge, the نمایش/زبان panel, and a «خروج» button that signs out on one tap, with no confirmation (unlike the customer hub). /fa/admin/systemas 09120000020: the identity card renders, but the role label under the name is the raw keyadmin.role_super_admin(see gaps). The six console rows render; every one of them 403s — that is the admin RBAC gap, not this flow./fa/partner/more— type the URL; nothing links to/fa/partner, and09120000030has no partner role (/me→["customer"]). Whatever center name you see is mock data.
Seeded-world caveat: every seeded customer already has an emergency contact, so the 400 in the first gap below will not reproduce on a demo account. To see it, sign in with a fresh phone, complete onboarding, then edit only the name.
Known gaps
- A customer with no emergency contact cannot save their name or language.
save()((customer)/profile/page.tsx:97-116) always posts the whole profile, andUpsertCustomerProfileCommand.Validator.cs:9-13requiresDefaultEmergencyContactNamenon-empty andDefaultEmergencyContactPhoneto be a valid Iranian mobile. Verified live:POST customer_profiles/upsertwith empty emergency fields → 400 ('Default Emergency Contact Name' must not be empty.). OnlysaveEmergencypre-validates (:120-123);savePersonal/saveLanguagedo not. - That 400 is completely silent.
useUpsertCustomerProfilehas noonError, the call site passes onlyonSuccess, andclientFetchdeliberately does not toast non-401/403/5xx 4xx (lib/api/client.ts:11). The sheet just stays open — a client hard-rule-22 violation. preferredLanguageis stored and never consumed. It round-trips server-side (CustomerProfileRepository.cs:25), but the only client reads are the form's own default and its save (profile/page.tsx:85,105). The UI locale comes from the URL via next-intl; nothing reads the stored preference at login or on the server. The زبان sheet therefore shows two language controls that do different things.- Nurse avatars never load.
LocalDiskObjectStorage.GetUrlreturns afile://URI (:51); liveGET /nurse_profiles/mefor nurse 1 returnsavatarUrl: "file:///C:/Users/.../avatars/nurse/1/....png", which a browser will not fetch from anhttp://page.ProfileSummarydegrades to the name's first letter. Affects the deployedlocalprovider too — onlySeams:ObjectStorage:Provider = s3would emit a usable URL. - The admin hub renders a raw translation key as the role label.
admin/system/page.tsx:79callsta(`role_${primaryRoleCode}`);admin.role_super_admin/role_finance/role_support/role_moderation/role_adminexist in neithermessages/fa.jsonnoren.json. NoonError/getMessageFallbackis configured ini18n/request.ts, so next-intl renders the key path.npm run checkmisses it —check-copy.mjslints orthography and en/fa symmetry, not key existence. - The partner settings hub shows fabricated identity.
USE_PARTNER_MOCK = trueand the mock resolvesMOCK_MY_CENTER_ID = 1for any caller, so the center name and the merchant-of-record chip are the same for everyone. There is no real tenancy on this screen. - Sign-out confirmation is inconsistent. The customer hub gates it behind a
ConfirmDialog(profile/page.tsx:259-271); nurse, admin and partner useSignOutRow, which fires on the first tap. One mis-tap on/fa/nurse/moreends the session — and, per the handler, every other session too. - A customer cannot set an avatar.
POST /api/v1/customer_profiles/avataris live but the client'suploadAvatartargets the nurse route only (profiles/apis/clientApi.ts:100-109); the customer hub passesinitialsFallbackand noavatarUrl. Already recorded in profiles.md. - No notification-preference surface exists. The «اعلانها» row deep-links to the notification centre; there is no per-channel opt-in/out anywhere in the app.
- The nurse hub's support badge can never show a number.
ticketsApi.getUnreadTotalon the real path is literallyasync () => null(tickets/apis/clientApi.ts:223, REQ-059), souseSupportUnreadTotal()always returnsnullandNurseMoreScreen.tsx:40renders no badge. - The customer hub navigates with a hand-built locale prefix.
profile/page.tsx:66,129importsuseRouterfromnext/navigationand doesrouter.push(`/${locale}${path}`)— client hard rule 13 requires@/i18n/navigation. Works today; breaks silently iflocalePrefixever changes. - UNVERIFIED (no browser in this environment): a system-mode user may see one scheme flip after
hydration.
getThemeModecollapses the cookie to a concretedefaultModeofdark/light(lib/cookies/server.ts:47-48), whileThemeModeSettingreads MUI'smode, which can still be'system'. If the OS scheme changed since the last visit, the SSR paint and the post-hydration resolution disagree. Code-traced only; not observed rendering.