From 2cd107528644693d37efdc490bd0bbb211e6967b Mon Sep 17 00:00:00 2001 From: hamid Date: Sun, 2 Aug 2026 21:03:26 +0330 Subject: [PATCH] fix blocker 1 - super admin --- mvp/blockers.md | 17 ------ mvp/fix-plan.md | 39 +++++++------- mvp/test-flows.md | 45 ++++++++-------- .../DynamicPermissionService.cs | 8 +-- .../SeedDatabaseService/SeedDataBase.cs | 48 ++++++++++++++++- .../DynamicPermissionServiceTest.cs | 54 +++++++++++++++++++ 6 files changed, 148 insertions(+), 63 deletions(-) create mode 100644 server/src/Tests/Baya.Test.Infrastructure.Identity/Baya.Test.Infrastructure.Identity/DynamicPermissionServiceTest.cs diff --git a/mvp/blockers.md b/mvp/blockers.md index 6b66a25..a465675 100644 --- a/mvp/blockers.md +++ b/mvp/blockers.md @@ -14,18 +14,6 @@ Effort is a rough size, not a schedule: **S** = small/contained, **M** = a real ## A. The product is broken or unusable -### Admin can't do anything -- **No admin account can perform a single real admin action.** Every super-admin/finance account gets - "access denied" on every real admin screen — approving a nurse, moderating a review, processing a refund, - running a payout batch, managing tickets, all of it. The permission system only recognizes one hardcoded - role name that nothing ever actually assigns. *(Effort: M)* -- **On top of that, no demo account holds even that hardcoded role**, so fixing the permission bug alone - still leaves nothing to test it with out of the box. *(Effort: S, bundled with the item above)* - - *Why this matters more than it looks: a large share of the other items below — nurse verification, - reviews, refunds, payouts, ticket handling — are gated behind this one fix. Fixing admin access is the - single highest-leverage thing on this list.* - ### Payments - **A card payment can never actually complete.** The test/demo payment page redirects to a page that doesn't exist. Nothing about the money logic itself is wrong — it's the very last step, handing off to a @@ -81,11 +69,6 @@ Effort is a rough size, not a schedule: **S** = small/contained, **M** = a real reach a state where the nurse can actually be paid for the visits she did complete. The "today's visits" list nurses see is also unfiltered — it shows a nurse's entire history, not just today. *(Effort: M)* -### Reviews -- **A real review can never actually go live.** It gets submitted and saved correctly, but the approval step - that would publish it sits behind the broken admin-access problem above, so nothing ever gets un-stuck. - *(Effort: S once admin access is fixed)* - ### Catalog / pricing - **Outside of the developer's own local setup, there are zero service categories or pricing options configured at all**, and there is no admin page to add them — only direct database editing. Deployed as-is, diff --git a/mvp/fix-plan.md b/mvp/fix-plan.md index bfd3ea4..e9ad5fd 100644 --- a/mvp/fix-plan.md +++ b/mvp/fix-plan.md @@ -3,8 +3,9 @@ An index into [`blocker-phases/`](blocker-phases/), an engineering-facing companion to [blockers.md](blockers.md): one numbered file per fix, in a suggested working order. Each file has the exact root cause (file:line), the proposed fix, and anywhere the fix depends on a business-rule decision that -shouldn't be guessed. Written from a full read of the current code (commit `fb58ca5`, 2026-08-02). Nothing has -been applied yet — work through them at your own pace, in whatever order you prefer. +shouldn't be guessed. Written from a full read of the current code (commit `fb58ca5`, 2026-08-02). Phases 01 +and 02 are applied (see Status column); the rest are unapplied — work through them at your own pace, in +whatever order you prefer. > **Two blockers already got a scope decision, recorded so it isn't re-litigated:** §B.4 (مودیان > e-invoicing) gets a note in [forgotten-features.md](forgotten-features.md) instead of a build — no chosen @@ -13,23 +14,23 @@ been applied yet — work through them at your own pace, in whatever order you p ## Phases -| # | File | Blocker | Depends on | -| --- | --- | --- | --- | -| 01 | [admin-rbac](blocker-phases/01-admin-rbac.md) | Admin can't do anything (the #1 leverage item) | — | -| 02 | [reviews-moderation](blocker-phases/02-reviews-moderation.md) | Reviews can never go live | 01 | -| 03 | [address-edit-bug](blocker-phases/03-address-edit-bug.md) | Editing an address wipes recipient name/phone | — | -| 04 | [payment-timezone](blocker-phases/04-payment-timezone.md) | The 30-minute payment countdown can lie | — | -| 05 | [bnpl-setup](blocker-phases/05-bnpl-setup.md) | Installments (BNPL) don't work at all | — | -| 06 | [catalog-admin-page](blocker-phases/06-catalog-admin-page.md) | No admin page for service categories/pricing | — | -| 07 | [card-payment-redirect](blocker-phases/07-card-payment-redirect.md) | Card payment can never complete | — | -| 08 | [refunds-demock](blocker-phases/08-refunds-demock.md) | Refunds are demo-only, off by 100× | — | -| 09 | [nurse-verification-badge](blocker-phases/09-nurse-verification-badge.md) | Verification badge doesn't reflect reality | pairs with 10 | -| 10 | [search-dedup-and-trust](blocker-phases/10-search-dedup-and-trust.md) | Search isn't de-duplicated; trust info hardcoded | pairs with 09 | -| 11 | [nurse-payouts](blocker-phases/11-nurse-payouts.md) | Nurse pay/payouts are fake, no "process" action | benefits from 01 | -| 12 | [patient-records](blocker-phases/12-patient-records.md) | Patient records & visit notes are fake demo data | needs a product decision first | -| 13 | [booking-lifecycle](blocker-phases/13-booking-lifecycle.md) | Stuck bookings; "today's visits" unfiltered | pairs with 04 | -| 14 | [partner-center](blocker-phases/14-partner-center.md) | Partner/business-center accounts are fake | benefits from 01 | -| 15 | [debug-mode-production](blocker-phases/15-debug-mode-production.md) | Turn off dev mode on the live site (§B.2) | do last, deliberately | +| # | File | Blocker | Depends on | Status | +| --- | --- | --- | --- | --- | +| 01 | [admin-rbac](blocker-phases/01-admin-rbac.md) | Admin can't do anything (the #1 leverage item) | — | ✅ Done | +| 02 | [reviews-moderation](blocker-phases/02-reviews-moderation.md) | Reviews can never go live | 01 | ✅ Done (no separate fix needed — unblocked by 01) | +| 03 | [address-edit-bug](blocker-phases/03-address-edit-bug.md) | Editing an address wipes recipient name/phone | — | — | +| 04 | [payment-timezone](blocker-phases/04-payment-timezone.md) | The 30-minute payment countdown can lie | — | — | +| 05 | [bnpl-setup](blocker-phases/05-bnpl-setup.md) | Installments (BNPL) don't work at all | — | — | +| 06 | [catalog-admin-page](blocker-phases/06-catalog-admin-page.md) | No admin page for service categories/pricing | — | — | +| 07 | [card-payment-redirect](blocker-phases/07-card-payment-redirect.md) | Card payment can never complete | — | — | +| 08 | [refunds-demock](blocker-phases/08-refunds-demock.md) | Refunds are demo-only, off by 100× | — | — | +| 09 | [nurse-verification-badge](blocker-phases/09-nurse-verification-badge.md) | Verification badge doesn't reflect reality | pairs with 10 | — | +| 10 | [search-dedup-and-trust](blocker-phases/10-search-dedup-and-trust.md) | Search isn't de-duplicated; trust info hardcoded | pairs with 09 | — | +| 11 | [nurse-payouts](blocker-phases/11-nurse-payouts.md) | Nurse pay/payouts are fake, no "process" action | benefits from 01 | — | +| 12 | [patient-records](blocker-phases/12-patient-records.md) | Patient records & visit notes are fake demo data | needs a product decision first | — | +| 13 | [booking-lifecycle](blocker-phases/13-booking-lifecycle.md) | Stuck bookings; "today's visits" unfiltered | pairs with 04 | — | +| 14 | [partner-center](blocker-phases/14-partner-center.md) | Partner/business-center accounts are fake | benefits from 01 | — | +| 15 | [debug-mode-production](blocker-phases/15-debug-mode-production.md) | Turn off dev mode on the live site (§B.2) | do last, deliberately | — | ## Suggested order diff --git a/mvp/test-flows.md b/mvp/test-flows.md index 21b6ae6..d2402a2 100644 --- a/mvp/test-flows.md +++ b/mvp/test-flows.md @@ -32,8 +32,8 @@ need to touch that directly except for one step below). | `09120000003` | Nurse — مریم احمدی (f) | **Not verified yet** — should never appear in customer search | | `09120000010` | Customer — سارا محمدی (f) | 2 people in her care circle, 1 saved address, owns most seeded bookings | | `09120000011` | Customer — رضا حسینی (m) | 1 infant patient, owns the installment-plan bookings | -| `09120000020` | Admin (full) — نگار مدیری (f) | Can log in, but admin actions don't work yet — see [blockers.md](blockers.md) | -| `09120000021` | Admin (finance) — کامران مالی (m) | Same admin caveat as above | +| `09120000020` | Admin (super_admin) — نگار مدیری (f) | Real actions now work in review moderation and the support ticket queue; most other admin consoles are still client-side sample data — see [blockers.md](blockers.md) | +| `09120000021` | Admin (finance) — کامران مالی (m) | Scoped to refunds/payouts/platform-settings consoles; same "most consoles still sample data" caveat as above | | `09120000030` | Customer who owns a partner business center — بهنام رستگار (m) | Logs in as a normal customer, then opens the partner section by typing its address directly — nothing in the app links to it | --- @@ -49,12 +49,12 @@ need to touch that directly except for one step below). 3. Read the code as described above and type it in. Expect it to auto-submit and take you to the family home screen (not the nurse page you originally tried). 4. Repeat with `09120000001` (زهرا عزیزی, nurse). This time you should land on the nurse's "Today's Visits" screen instead. 5. Try entering a wrong code five times in a row. Expect a lockout message; requesting a new code should still work. -6. Log in as `09120000020` (نگار مدیری, admin). Login itself succeeds, but admin features don't work (known blocker). +6. Log in as `09120000020` (نگار مدیری, admin). Login succeeds and lands on the admin home; most admin consoles still show sample data (client-side mock, unrelated to login). **What you should see:** each account type lands on its correct home screen after login, and a page you tried to reach before logging in is remembered and opened afterward. **Known issues:** -- Admin accounts can log in but can't actually do anything yet (known blocker — see [blockers.md](blockers.md)) +- Most admin consoles still show client-side sample data regardless of login (see "Admin backoffice" below) - After too many wrong codes, the "try again later" message is wrongly shown as "wrong code" - Logging out on one device signs that account out everywhere, with no option to choose just one device @@ -344,19 +344,21 @@ The screens you'll see show fake numbers, not what you would actually get back. - Think twice before pressing the final "confirm cancellation" button on a demo account's only remaining bookings — it permanently cancels them for everyone testing with that account ### Rating and reviewing a nurse after a visit -**Who tests this:** customers (write a review); admins (approve reviews — currently broken); anyone (reads published reviews) +**Who tests this:** customers (write a review); admins (approve reviews); anyone (reads published reviews) **Status today:** ⚠️ Works, but has known issues 1. Log in as سارا محمدی (`09120000010`). 2. Go to Bookings — only one completed booking shows a "write a review" (ثبت نظر) button; the others already have a review or aren't eligible. 3. Tap it, give a 1–5 star rating, optionally add a note and pick some tags, then submit — a notice warns it must be approved before anyone else can see it. 4. The screen then shows your review as "pending review" (در انتظار بررسی) — this part is genuinely real and saved. -5. Check a nurse's profile "Reviews" tab — only already-approved reviews show up; yours won't appear, because it's stuck waiting on approval. +5. Check a nurse's profile "Reviews" tab — only already-approved reviews show up; yours won't appear yet. +6. Log in as `09120000020` (نگار مدیری, super_admin) and open the review moderation queue (`/fa/admin/reviews`). Expect your pending review to show up for real, with approve/reject actions that now actually work. +7. Approve it, then re-check the nurse's profile "Reviews" tab — it should now appear. -**What you should see:** submitting a review actually works for real, but it never becomes publicly visible in this demo because the approval step is broken. +**What you should see:** submitting a review, approving it as admin, and seeing it go live are all genuinely real and connected end-to-end. **Known issues:** -- Reviews can never actually get published yet — stuck waiting on admin approval, which is broken (known blocker — see [blockers.md](blockers.md)) +- `ReviewModerationStatus.Rejected` is reachable only through this admin action — the submit-time banned-word filter maps to "hidden," not "rejected" - A nurse has no screen yet to see reviews written about them ### Nurse earnings and payouts @@ -390,12 +392,11 @@ Everything on these screens is fake, even though real payout numbers do exist be 3. Open the support conversation. Expect just the customer/staff messages — any internal staff-only notes never appear here. 4. Type a reply and send it. Expect it to appear immediately, then confirm as sent. 5. Log in as the nurse, `09120000001` (زهرا عزیزی), and open her support/messages page — same kind of list, nurse's own threads. -6. Log in as an admin (`09120000020`) and try the admin support-queue page. Expect it to fail with an access-denied error (known blocker). +6. Log in as an admin (`09120000020`, super_admin) and open the admin support-queue page. Expect the real ticket list to load and a conversation to open. -**What you should see:** customers and nurses can view and reply to their own threads normally; the admin/staff side of messaging is currently unreachable. +**What you should see:** customers, nurses, and now the admin/staff side all view and reply to threads for real. **Known issues:** -- Admin ticket queue and admin view of a conversation are broken (access denied) (known blocker — see [blockers.md](blockers.md)) - The little unread-messages counter in the header never lights up - The inbox list doesn't show a preview of the latest message - Auto-created booking conversations start completely empty, with nothing explaining what they're for @@ -425,22 +426,22 @@ Everything on these screens is fake, even though real payout numbers do exist be ### Admin backoffice **Who tests this:** admin accounts (super_admin and finance) -**Status today:** ❌ Mostly fake/broken — not really testable yet +**Status today:** ⚠️ Two consoles are real; the rest is still a client-side mock -Right now every admin account gets an "access denied" error on essentially every real admin action on the -server — this is one of the biggest known blockers. The admin screens in the app look fully populated and -functional, but that's because almost all of them are showing made-up sample data built into the app, not -real information. If that fake layer weren't there, clicking any real action would fail with access denied. +The server-side permission bug that blocked every admin action is fixed. Two consoles were already wired to +a real (non-admin-mock) domain and now work end-to-end: review moderation and the support ticket queue. The +other admin sections still render from `client/src/services/admin`'s built-in sample data regardless of who's +logged in — that's a separate, larger de-mock effort tracked per-console in +[fix-plan.md](fix-plan.md) (phases 09–14 touch the ones that matter most). 1. Log in as an admin, either `09120000020` (نگار مدیری, super_admin) or `09120000021` (کامران مالی, finance). -2. Browse the admin sections: nurse verification, review moderation, payouts, refunds, support tickets/alerts, platform settings, holidays, audit log, and partner centers. -3. Expect most sections to show full, editable-looking data, and "saving" a change to appear to work — but this is all fake, local sample data, not connected to the real system. -4. Two sections (the ticket queue and review moderation) actually try to reach the server, and both show a visible error instead of data. +2. Open review moderation (`/fa/admin/reviews`) and the support ticket queue (`/fa/admin/tickets`) — both load real data and their actions (approve/reject a review, reply to/close a ticket) actually work. +3. Browse the rest: nurse verification, payouts, refunds, support alerts, platform settings, holidays, audit log, and partner centers. Expect full, editable-looking data, and "saving" a change to appear to work — but this is still fake, local sample data, not connected to the real system. -**What you should see:** a good-looking dashboard that mostly isn't real. Only two screens honestly show that something is broken; the rest hide it. +**What you should see:** review moderation and the ticket queue behave like a normal, working admin tool; every other section still looks real but isn't connected yet. **Known issues:** -- Admin accounts can't do anything for real yet (known blocker — see [blockers.md](blockers.md)) +- 19 of 21 admin consoles still show client-side sample data, not the real system (see [fix-plan.md](fix-plan.md)) - The "user directory" and "roles" admin screens have no real data behind them at all ### Partner center @@ -473,7 +474,7 @@ This section is 100% fake demo data — there's no real business-center account 5. Edit your personal info (e.g. last name) and save. Expect a "saved" confirmation and the name to update on screen. 6. Tap sign out («خروج از حساب») and confirm. Expect to land on the login screen, with that account fully signed out everywhere, not just this device. 7. Log in as the nurse, `09120000001` (زهرا عزیزی), and open her account/settings menu. Expect similar settings, but sign-out here happens on a single tap with no confirmation — be careful. -8. As an admin, the account page shows a broken, untranslated label for the admin's role, and every admin section link on it leads to an access-denied error (known blocker). +8. As an admin, the account page shows a broken, untranslated label for the admin's role. Admin section links now open normally (most render sample data rather than erroring — see "Admin backoffice" above). 9. The partner account page is only reachable by typing its address manually and shows a fake business name (known blocker). **What you should see:** appearance, language switching, editing personal info, and sign-out all genuinely work for a customer and nurse; admin and partner settings inherit the blockers from those areas. diff --git a/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs b/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs index 3a1778d..dae8622 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/PermissionManager/DynamicPermissionService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Baya.Domain.Entities.User; namespace Baya.Infrastructure.Identity.Identity.PermissionManager; @@ -6,18 +7,17 @@ public class DynamicPermissionService : IDynamicPermissionService { public bool CanAccess(ClaimsPrincipal user, string area, string controller, string action) { - if (user.IsInRole("admin")) + // super_admin is definitionally the top role, but RoleNames has no hierarchy — each admin + // sub-role is a sibling, so it must be granted the bypass explicitly, not implied by "admin". + if (user.IsInRole(RoleNames.Admin) || user.IsInRole(RoleNames.SuperAdmin)) { return true; } - var key = $"{area}:{controller}:"; var userClaims = user.FindAll(ConstantPolicies.DynamicPermission); return userClaims.Any(item => item.Value.Equals(key, StringComparison.OrdinalIgnoreCase)); } - - } \ No newline at end of file diff --git a/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs b/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs index f1175e2..e629020 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs @@ -1,5 +1,8 @@ +using Baya.Application.Contracts.Identity; +using Baya.Application.Models.Identity; using Baya.Domain.Entities.User; using Baya.Infrastructure.Identity.Identity.Manager; +using Baya.Infrastructure.Identity.Identity.PermissionManager; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -15,12 +18,25 @@ public class SeedDataBase : ISeedDataBase private readonly AppUserManager _userManager; private readonly AppRoleManager _roleManager; private readonly IConfiguration _configuration; + private readonly IRoleManagerService _roleManagerService; - public SeedDataBase(AppUserManager userManager, AppRoleManager roleManager, IConfiguration configuration) + // The admin-console permission matrix owned by each non-admin sub-role — mirrors + // client/src/hooks/capabilities.ts (useAdminCapabilities), the reviewed source of truth for who + // owns which console. Area is always empty: no controller uses [Area] today. + private static readonly IReadOnlyDictionary AdminSubRolePermissions = + new Dictionary + { + [RoleNames.Finance] = ["AdminRefunds", "AdminPayouts", "PlatformConfig"], + [RoleNames.Support] = ["AdminVerifications", "AdminVerificationStepTypes", "SupportAlerts", "AdminTickets"], + [RoleNames.Moderation] = ["AdminReviews", "Reviews"], + }; + + public SeedDataBase(AppUserManager userManager, AppRoleManager roleManager, IConfiguration configuration, IRoleManagerService roleManagerService) { _userManager = userManager; _roleManager = roleManager; _configuration = configuration; + _roleManagerService = roleManagerService; } public async Task Seed() @@ -38,9 +54,39 @@ public class SeedDataBase : ISeedDataBase } } + await SeedAdminSubRolePermissionsAsync(); await SeedBootstrapAdminAsync(); } + // finance/support/moderation are sibling roles, not implied by super_admin — each needs its own + // DynamicPermission claims scoped to the consoles it owns. Skips roles already at the target claim + // set so a normal restart doesn't churn every affected user's security stamp. + private async Task SeedAdminSubRolePermissionsAsync() + { + foreach (var (roleName, controllers) in AdminSubRolePermissions) + { + var role = await _roleManager.FindByNameAsync(roleName); + if (role == null) + continue; + + var targetPermissions = controllers.Select(controller => $":{controller}:").ToList(); + + var existingPermissions = (await _roleManager.GetClaimsAsync(role)) + .Where(c => c.Type == ConstantPolicies.DynamicPermission) + .Select(c => c.Value) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (existingPermissions.SetEquals(targetPermissions)) + continue; + + await _roleManagerService.ChangeRolePermissionsAsync(new EditRolePermissionsDto + { + RoleId = role.Id, + Permissions = targetPermissions + }); + } + } + // The bootstrap admin is config-driven, never a committed credential: it is created only when both // Seed:AdminUsername and Seed:AdminPassword are supplied (via the environment-specific appsettings file, environment // variables in a deployment). With neither configured — the default for Testing and any fresh boot — diff --git a/server/src/Tests/Baya.Test.Infrastructure.Identity/Baya.Test.Infrastructure.Identity/DynamicPermissionServiceTest.cs b/server/src/Tests/Baya.Test.Infrastructure.Identity/Baya.Test.Infrastructure.Identity/DynamicPermissionServiceTest.cs new file mode 100644 index 0000000..d87e40c --- /dev/null +++ b/server/src/Tests/Baya.Test.Infrastructure.Identity/Baya.Test.Infrastructure.Identity/DynamicPermissionServiceTest.cs @@ -0,0 +1,54 @@ +using System.Security.Claims; +using Baya.Domain.Entities.User; +using Baya.Infrastructure.Identity.Identity.PermissionManager; + +namespace Baya.Test.Infrastructure.Identity +{ + public class DynamicPermissionServiceTest + { + private readonly DynamicPermissionService _sut = new(); + + [Fact] + public void Admin_Role_Bypasses_Permission_Check() + { + var user = BuildUser(RoleNames.Admin); + + Assert.True(_sut.CanAccess(user, area: null, controller: "AdminRefunds", action: "Create")); + } + + [Fact] + public void SuperAdmin_Role_Bypasses_Permission_Check() + { + var user = BuildUser(RoleNames.SuperAdmin); + + Assert.True(_sut.CanAccess(user, area: null, controller: "AdminRefunds", action: "Create")); + } + + [Fact] + public void Role_With_Matching_Claim_Is_Granted_Access() + { + var user = BuildUser(RoleNames.Finance, dynamicPermission: ":AdminRefunds:"); + + Assert.True(_sut.CanAccess(user, area: null, controller: "AdminRefunds", action: "Create")); + } + + [Fact] + public void Role_Without_Matching_Claim_Is_Denied_Access() + { + var user = BuildUser(RoleNames.Finance, dynamicPermission: ":AdminRefunds:"); + + Assert.False(_sut.CanAccess(user, area: null, controller: "AdminReviews", action: "Status")); + } + + private static ClaimsPrincipal BuildUser(string role, string? dynamicPermission = null) + { + var claims = new List { new(ClaimTypes.Role, role) }; + + if (dynamicPermission != null) + claims.Add(new Claim(ConstantPolicies.DynamicPermission, dynamicPermission)); + + var identity = new ClaimsIdentity(claims, "TestAuth", ClaimTypes.Name, ClaimTypes.Role); + return new ClaimsPrincipal(identity); + } + } +}