Files
baya-monorepo/docs/flows/account-and-settings.md
T
2026-08-02 17:18:36 +03:30

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.tsxProfileSummary + 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.tsxProfileSummary (+ 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.tsxThemeModeSetting (روشن/تیره/سیستم) + 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() mockedUSE_PARTNER_MOCK = true, and the mock resolves MOCK_MY_CENTER_ID = 1 for any caller
sign out POST /api/v1/auth/logout AuthController.cs:48LogoutCommand.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:269useLogout()authApi.logout({})authClientApi.logout (auth/apis/clientApi.ts:47-52) → POST /api/v1/auth/logoutLogoutCommand.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.tsclearAuthTokens()), 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 refreshTokenEverywhere. 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

  1. Log in as 09120000010 (سارا محمدی, customer) — see 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, not this flow.
  9. /fa/partner/moretype 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.
  • 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.