# 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](../../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](../integration/domains/profiles.md) · [docs/integration/domains/auth.md](../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`](../../client/src/app/%5Blocale%5D/%28private-routes%29/%28customer%29/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`](../../client/src/app/%5Blocale%5D/%28private-routes%29/nurse/more/NurseMoreScreen.tsx) — `ProfileSummary` (+ `TrustBadge`), support/notification hub rows, `SettingsPanel`, `SignOutRow` | | Admin hub | `/fa/admin/system` | [`admin/system/page.tsx`](../../client/src/app/%5Blocale%5D/%28private-routes%29/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`](../../client/src/app/%5Blocale%5D/%28private-routes%29/partner/more/page.tsx) — center name + MoR chip, `SettingsPanel`, `SignOutRow` | | Appearance + language | (in every hub) | [`SettingsPanel.tsx`](../../client/src/components/settings/SettingsPanel.tsx) → [`ThemeModeSetting`](../../client/src/components/settings/ThemeModeSetting.tsx) (روشن/تیره/سیستم) + [`LocaleSwitcher`](../../client/src/components/common/LocaleSwitcher/LocaleSwitcher.tsx). **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`](../../client/src/layout/components/ActorSwitcher.tsx) — 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](../integration/domains/profiles.md), [auth.md](../integration/domains/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`](../../server/src/Core/Baya.Application/Features/Identity/Commands/Logout/LogoutCommand.Handler.cs). 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](../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 `` 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 1. Log in as **09120000010** (سارا محمدی, customer) — see [testing-setup.md](testing-setup.md). 2. Open `/fa/profile`. **Expect:** initials avatar, «سارا محمدی», masked `0912*****10`, a «تماس اضطراری» card with a **green** check icon reading «بهرام محمدی · 09121110010», and rows for اطلاعات شخصی / نشانی‌ها / زبان / نمایش / اعلان‌ها / پشتیبانی, then a red «خروج از حساب» row. 3. Tap the **نمایش** segments روشن → تیره. **Expect:** the whole app repaints immediately, no reload; reload the page and the choice survives (the `color-scheme` cookie), with no flash of the wrong scheme. 4. Tap **زبان** → the globe button. **Expect:** the URL becomes `/en/profile` — the *same* page, not home. The «زبان برنامه» select below it saves `preferredLanguage` to the server but **changes nothing visible** (see gaps). 5. Tap **اطلاعات شخصی**, change the family name, save. **Expect:** a «ذخیره شد» toast and the header name updates (the mutation invalidates `/me`). 6. Tap **خروج از حساب** → confirm «خروج از حساب؟». **Expect:** you land on `/fa/login`. Re-using that account's old bearer token against `GET /api/v1/me` now returns `401`. 7. 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). 8. `/fa/admin/system` as **09120000020**: the identity card renders, but the role label under the name is the raw key `admin.role_super_admin` (see gaps). The six console rows render; every one of them 403s — that is the [admin RBAC gap](testing-setup.md#-the-seeded-admins-cannot-reach-any-admin-endpoint), not this flow. 9. `/fa/partner/more` — **type the URL**; nothing links to `/fa/partner`, and `09120000030` has 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, and `UpsertCustomerProfileCommand.Validator.cs:9-13` requires `DefaultEmergencyContactName` non-empty and `DefaultEmergencyContactPhone` to be a valid Iranian mobile. Verified live: `POST customer_profiles/upsert` with empty emergency fields → **400** (`'Default Emergency Contact Name' must not be empty.`). Only `saveEmergency` pre-validates (`:120-123`); `savePersonal`/`saveLanguage` do not. - **That 400 is completely silent.** `useUpsertCustomerProfile` has no `onError`, the call site passes only `onSuccess`, and `clientFetch` deliberately does not toast non-401/403/5xx 4xx (`lib/api/client.ts:11`). The sheet just stays open — a client hard-rule-22 violation. - **`preferredLanguage` is 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.GetUrl` returns a `file://` URI (`:51`); live `GET /nurse_profiles/me` for nurse 1 returns `avatarUrl: "file:///C:/Users/.../avatars/nurse/1/....png"`, which a browser will not fetch from an `http://` page. `ProfileSummary` degrades to the name's first letter. Affects the deployed `local` provider too — only `Seams:ObjectStorage:Provider = s3` would emit a usable URL. - **The admin hub renders a raw translation key as the role label.** `admin/system/page.tsx:79` calls ``ta(`role_${primaryRoleCode}`)``; `admin.role_super_admin` / `role_finance` / `role_support` / `role_moderation` / `role_admin` exist in **neither** `messages/fa.json` nor `en.json`. No `onError`/ `getMessageFallback` is configured in `i18n/request.ts`, so next-intl renders the key path. `npm run check` misses it — `check-copy.mjs` lints orthography and en/fa symmetry, not key existence. - **The partner settings hub shows fabricated identity.** `USE_PARTNER_MOCK = true` and the mock resolves `MOCK_MY_CENTER_ID = 1` for **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 use `SignOutRow`, which fires on the first tap. One mis-tap on `/fa/nurse/more` ends the session — and, per the handler, every other session too. - **A customer cannot set an avatar.** `POST /api/v1/customer_profiles/avatar` is live but the client's `uploadAvatar` targets the nurse route only (`profiles/apis/clientApi.ts:100-109`); the customer hub passes `initialsFallback` and no `avatarUrl`. Already recorded in [profiles.md](../integration/domains/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.getUnreadTotal` on the real path is literally `async () => null` (`tickets/apis/clientApi.ts:223`, REQ-059), so `useSupportUnreadTotal()` always returns `null` and `NurseMoreScreen.tsx:40` renders no badge. - **The customer hub navigates with a hand-built locale prefix.** `profile/page.tsx:66,129` imports `useRouter` from `next/navigation` and does ``router.push(`/${locale}${path}`)`` — client hard rule 13 requires `@/i18n/navigation`. Works today; breaks silently if `localePrefix` ever changes. - **UNVERIFIED (no browser in this environment): a system-mode user may see one scheme flip after hydration.** `getThemeMode` collapses the cookie to a concrete `defaultMode` of `dark`/`light` (`lib/cookies/server.ts:47-48`), while `ThemeModeSetting` reads MUI's `mode`, 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.