Files
baya-monorepo/dev/post-phase/hardening/issues.md
T
2026-07-17 13:22:04 +03:30

12 KiB

Hardening issue ledger (verified findings)

Every item below was found by one audit agent and re-confirmed by an independent adversarial verifier reading the code (and, for H-01, probing the running dev server). Evidence is file:line at the time of the audit (2026-07-16) — re-locate if lines drifted, but do not re-litigate the finding.

This file is the loop's progress state. When a phase fixes an item, tick it [x] and append — fixed in <commit> on the same line. LOOP-PROMPT.md reads this file to decide what's left.


Phase 0 — auth gate & session liveness

  • H-01 (blocker) — the auth gate never executes at runtime. client/middleware.ts is correct as written (PUBLIC_PATHS = ['/login'], redirect on dead token) but never runs: live cookie-less GET /fa/admin → 200 full HTML, bare GET / → 404 (no next-intl locale redirect either) — reproduced under Turbopack and --webpack. Cause: Next.js infers the workspace root from a stray C:\Users\Lenovo\pnpm-lock.yaml (startup warning names it), so the middleware file is never bound. Also: Next 16 deprecates middleware.ts for proxy.ts, and the docs place the file "at the same level as pages or app" — for this repo that's client/src/. Fix: pin turbopack.root (+ outputFileTracingRoot) in client/next.config.mjs; migrate the file to src/proxy.ts; prove the gate runs with a runtime probe (curl DoD).

  • H-02 (blocker) — isTokenAlive can never read the real token (JWE). Server: server/src/Infrastructure/Baya.Infrastructure.Identity/Jwt/JwtService.cs:116-128EncryptingCredentials(Aes128KW, Aes128CbcHmacSha256) ⇒ the access token is a 5-segment JWE. Client: client/src/lib/auth/token.ts:24-38 JSON-parses segment 1 ⇒ always nullisTokenAlive always false for real tokens. Consequences: (a) once H-01 is fixed the middleware would redirect-loop logged-in users; (b) getServerAuthState (client/src/lib/auth/server.ts) seeds isAuthenticated=false on every hard reload. The wire already returns accessExpiresAt/refreshExpiresAt (client/src/services/auth/types.ts:62-66): persist a readable companion expiry cookie in persistAuthTokens and check that instead of decoding.

  • H-03 (high) — anonymous visitor to a private shell = infinite splash, never a login ask. useMe() is enabled only when isAuthenticated; for a cookie-less session that's permanently false, so useRoleHydration() never leaves loading and RoleGuard (client/src/components/auth/RoleGuard.tsx:55) renders AuthSplash forever (splash markup confirmed in served HTML). Add an explicit unauthenticated state → redirect to /login?next=… with return-URL honored post-login.

Phase 1 — admin RBAC & staff role scopes (server)

  • H-04 (blocker) — DynamicPermission RBAC is structurally dead; the whole admin API 403s. DynamicPermissionService.cs:9 passes only the literal role "admin"; the seeded personas hold super_admin/finance (DemoWorldDefinitions.cs:144-145, distinct constants RoleNames.cs:13,17). No RoleClaim is ever seeded, and the only claim writer (RoleManagerService.ChangeRolePermissionsAsync, RoleManagerService.cs:145-208) has no HTTP surface (the Features/{Role,Admin} handlers exist but no controller exposes them). Every [Authorize(ConstantPolicies.DynamicPermission)] endpoint (all Admin* controllers, PlatformConfig, Holidays, Audit, SupportAlerts, Reviews-moderate) 403s for both seeded admins. RUNBOOK.md:115-130 and refinement-phase-2-report falsely document these personas as working — fix the docs too.

  • H-05 (high) — BookingRoles.Admin over-grants: Support/Moderation get clinical + financial access. Features/Bookings/BookingRoles.cs:8-9 bundles Admin/SuperAdmin/Support/Finance/Moderation; used by GetCareInstructionsQuery.Handler.cs:31,37 (encrypted clinical notes), TransitionBookingStatusCommand.Handler.cs:24-25,51-54 (force Completed ⇒ arms the payout dispute-window), GetNursePayableBalanceQuery.Handler.cs:22-24 (contradicts its own comment). Controllers carry only blanket [Authorize] — the handler set IS the boundary. The codebase's own convention is narrower (PatientAccess.cs:17 = [Admin, SuperAdmin]; StaffRoles.cs:5-13 "narrowest fitting scope"). Split into ClinicalAccess / Financial sets and update the handlers.

Phase 2 — money-path mock integrity (client)

  • H-06 (blocker) — refunds mock reads the retired bookings-mock store → real cancel 404s. services/refunds/apis/mockApi.ts:3,88 imports mockGetBookingForRefund from services/bookings/apis/mockApi.ts whose fixture array (ids ~5001-5005, :340-345 throws 404 otherwise) is orphaned since USE_BOOKINGS_MOCK=false. Cancel CTA renders for every real booking (bookings/[id]/page.tsx:79-104) then the preview errors (bookings/[id]/cancel/page.tsx:50-78).

  • H-07 (blocker) — BNPL wizard runs on a disconnected mock store keyed by the real request id. checkout/page.tsx:213-224 pushes the real requestId (payment+bookingRequests are real) into the BNPL branch; services/bnpl/apis/mockApi.ts:5-8 hard-imports bookingRequestsMockApi + mockInsertConvertedBooking (raw mock modules, bypassing both domains' seams); store holds only fixture ids 1-2 → 404, or on id collision settle() (:260-317) fabricates a client-side "booking" while the real request expires unpaid. BNPL_ENABLED=true (services/payment/constants.ts:23) has no gating. Stopgap: hide/derive the CTA while USE_BNPL_MOCK && !USE_BOOKING_REQUESTS_MOCK; any surviving mock must resolve requests through the real bookingRequestsApi selector.

  • H-08 (blocker) — verification is 100% mocked while catalog/search are real → invisible nurses. services/verification/constants.ts:8-9 USE_VERIFICATION_MOCK=true gates the WHOLE VerificationApi (nurse flow + TrustBadge + admin queue, one interface — types.ts:255-265). The mock's __mockApproveAll never flips the real nurse_profiles.is_verified (NurseProfile.cs:44-53, private setter, only the real b6 VerificationAggregator calls it), but variant activation is real and unconditional (SetVariantActiveCommand.Handler.cs:15-36), and the search gate requires IsVerified (SearchIndexMaintainer.cs:34,65,94,152). PublishGate.tsx:59-68 CTA makes no API call at all. Fix: split the seam (nurse half real now — clientApi is 1:1 per the registry; admin half stays mocked until REQ-034) and wire/gate the publish UI honestly.

  • H-09 (high) — nurse earnings fabricated although its endpoints are live. services/payouts/constants.ts:13 USE_PAYOUTS_MOCK=true, but REQ-025 was deliveredNursePayoutsController.cs:33-48 serves earnings_balance/earnings/{id}, and payouts/apis/clientApi.ts:163-187 already implements them. The flag is held hostage by the admin batch methods (REQ-036, still open). Split the seam per-method (nurse reads real, admin mock).

  • H-10 (medium) — payment outcome discards the delivered bookingId. services/payment/apis/clientApi.ts:55-75 types the response as Omit<…,'bookingId'> and sets bookingId: null (line 72) although BookingRequestDto.cs:46-49 carries it (REQ-017 delivered). Kills the confirmation deep links (checkout/return/page.tsx:84, confirmation/page.tsx:94-110) and the targeted cache invalidation (invalidations.ts:17-24). Read it off the wire; drop the stale REQ-017-pending comments.

Phase 3 — session & error-surface hardening (client)

  • H-11 (high) — logout/login never clear the query cache → cross-account data leak. useLogout.ts:22-30 removes only authKeys.all (['auth']); useVerifyOtp.ts:25-32 / useSelectRole.ts:19-38 touch only auth keys; queryClient.ts:17-27 is a tab-lifetime singleton; domain keys carry no user id. User B logging in after User A on the same device is served A's cached patients/bookings/addresses/tickets until staleTime lapses. Fix: queryClient.clear() on logout + reset on login.

  • H-12 (high) — raw stack-trace error boundary; no route-level error pages. components/common/ErrorBoundary.tsx:35-58 renders hardcoded-English error.toString() + full component stack, no retry, TODO-only reporting; used at CustomerLayout.tsx:78 and TopBarAndSideBarLayout.tsx:104 — and sits BELOW RoleGuard/providers, while no error.tsx or global-error.tsx exists anywhere under src/app. Replace with a branded i18n recovery card (mirror AuthAccountError), dev-only stack, and add [locale] route-level error files.

  • H-13 (high) — 401/403/5xx/network toasts are hardcoded English. lib/api/client.ts:57,80,86,91 — four dispatchToast('…English…') literals pass through dispatchToast.ts/ToastBridge.tsx verbatim; no keys exist in either messages file. Violates the project's golden rule #3 on the fa-default app. Localize via a locale-keyed dictionary (non-React call site — the fetch layer already computes the locale).

  • H-14 (medium) — admin read-only consoles unguarded client-side. admin/audit/page.tsx:25, admin/verification/page.tsx:50, admin/tickets/page.tsx:43, admin/roles/page.tsx:50,121 fetch/render with no useAdminCapabilities() check — only the sidebar (AdminLayout.tsx:24-33) hides them. (Server enforcement exists but is the dead DynamicPermission policy — H-04.) Add a CapabilityGuard early-return with the non-leaking access-denied pattern.

Phase 4 — contract completion batch (server; Tier B/C REQs)

  • H-15 (medium) — the deferred REQ set, per the ledger (for-backend.md — re-check each status before building; REQ-031's HTTP surface may already exist after Phase 1): REQ-019/020/021 customer refunds (cancel command, policy preview + canonical cancellation_policy_code set, refund-by-booking + decomposition) · REQ-022/023/024 BNPL (options/schedule/wallet-installments, eligibility KYC fields) · REQ-026 reviews (eligibility/my-review reads — verify what phase 3/4 already delivered) · REQ-027 leftovers (record_access check; the GET/PUT care_record endpoints already exist — PatientCareRecordsController.cs:44-54) · REQ-029/030 admin config updatedAt/updatedBy + audit filters · REQ-034 admin verification queue/signed-doc-URL/approve · REQ-035 admin refund preview/initiate/approve/reject · REQ-036 admin payout preview/run/retry/record-reference.

  • H-16a (medium, server half) — partner portal has no identity signal or self-scoped reads. REQ-038: administersPartnerCenterId on MeResult + a seeded demo partner admin; REQ-032/033: centers/me[/nurses|/bookings|/settlement] split reads + invoice totalIrr. Me interface (services/auth/types.ts:79-91) has no partner field; resolveRoleDestination (services/auth/routing.ts:32-47) has no partner branch.

Phase 5 — final de-mock & partner reachability (client)

  • H-16b (medium, client half) — partner routing + portal gate + flip USE_PARTNER_MOCK. Add the partner branch to resolveRoleDestination off the new /me signal; gate the portal at the layout level (today partner/layout.tsx:15 RoleGuard has no expected, the mock resolves a hardcoded center for ANY caller — partnerCenter/apis/mockApi.ts:237-243 — and nurses/bookings pages don't even await useMyPartnerCenter).

  • H-17 (medium) — patientRecords id-type mismatch; flip the remaining flags. Client Medication/RoutineItem/CareTask.id: string (patientRecords/types.ts:36,45,53, mock seeds 'm1'/'r1'/'t1') vs wire long Id (CarePlanDtos.cs:11-15) — a naive flip 400s every family-record save; new items should omit id (server assigns). Then flip every remaining USE_*_MOCK (refunds, bnpl, payouts-admin, admin, partnerCenter, verification-admin, patientRecords) as its REQs land; delete the orphaned cross-mock helpers; update mocks-registry.md + client/CLAUDE.md.