fix blocker 1 - super admin
This commit is contained in:
@@ -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
|
## 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
|
### Payments
|
||||||
- **A card payment can never actually complete.** The test/demo payment page redirects to a page that
|
- **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
|
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"
|
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)*
|
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
|
### Catalog / pricing
|
||||||
- **Outside of the developer's own local setup, there are zero service categories or pricing options
|
- **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,
|
configured at all**, and there is no admin page to add them — only direct database editing. Deployed as-is,
|
||||||
|
|||||||
+20
-19
@@ -3,8 +3,9 @@
|
|||||||
An index into [`blocker-phases/`](blocker-phases/), an engineering-facing companion to
|
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
|
[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
|
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
|
shouldn't be guessed. Written from a full read of the current code (commit `fb58ca5`, 2026-08-02). Phases 01
|
||||||
been applied yet — work through them at your own pace, in whatever order you prefer.
|
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 (مودیان
|
> **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
|
> 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
|
## Phases
|
||||||
|
|
||||||
| # | File | Blocker | Depends on |
|
| # | File | Blocker | Depends on | Status |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| 01 | [admin-rbac](blocker-phases/01-admin-rbac.md) | Admin can't do anything (the #1 leverage item) | — |
|
| 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 |
|
| 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 | — |
|
| 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 | — |
|
| 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 | — |
|
| 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 | — |
|
| 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 | — |
|
| 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× | — |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
## Suggested order
|
||||||
|
|
||||||
|
|||||||
+23
-22
@@ -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 |
|
| `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 |
|
| `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 |
|
| `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) |
|
| `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) | Same admin caveat as above |
|
| `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 |
|
| `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).
|
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.
|
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.
|
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.
|
**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:**
|
**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"
|
- 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
|
- 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
|
- 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
|
### 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
|
**Status today:** ⚠️ Works, but has known issues
|
||||||
|
|
||||||
1. Log in as سارا محمدی (`09120000010`).
|
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.
|
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.
|
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.
|
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:**
|
**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
|
- A nurse has no screen yet to see reviews written about them
|
||||||
|
|
||||||
### Nurse earnings and payouts
|
### 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.
|
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.
|
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.
|
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:**
|
**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 little unread-messages counter in the header never lights up
|
||||||
- The inbox list doesn't show a preview of the latest message
|
- 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
|
- 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
|
### Admin backoffice
|
||||||
**Who tests this:** admin accounts (super_admin and finance)
|
**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
|
The server-side permission bug that blocked every admin action is fixed. Two consoles were already wired to
|
||||||
server — this is one of the biggest known blockers. The admin screens in the app look fully populated and
|
a real (non-admin-mock) domain and now work end-to-end: review moderation and the support ticket queue. The
|
||||||
functional, but that's because almost all of them are showing made-up sample data built into the app, not
|
other admin sections still render from `client/src/services/admin`'s built-in sample data regardless of who's
|
||||||
real information. If that fake layer weren't there, clicking any real action would fail with access denied.
|
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).
|
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.
|
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. 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.
|
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.
|
||||||
4. Two sections (the ticket queue and review moderation) actually try to reach the server, and both show a visible error instead of data.
|
|
||||||
|
|
||||||
**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:**
|
**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
|
- The "user directory" and "roles" admin screens have no real data behind them at all
|
||||||
|
|
||||||
### Partner center
|
### 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.
|
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.
|
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.
|
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).
|
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.
|
**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.
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,5 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using Baya.Domain.Entities.User;
|
||||||
|
|
||||||
namespace Baya.Infrastructure.Identity.Identity.PermissionManager;
|
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)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var key = $"{area}:{controller}:";
|
var key = $"{area}:{controller}:";
|
||||||
|
|
||||||
var userClaims = user.FindAll(ConstantPolicies.DynamicPermission);
|
var userClaims = user.FindAll(ConstantPolicies.DynamicPermission);
|
||||||
|
|
||||||
return userClaims.Any(item => item.Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
return userClaims.Any(item => item.Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+47
-1
@@ -1,5 +1,8 @@
|
|||||||
|
using Baya.Application.Contracts.Identity;
|
||||||
|
using Baya.Application.Models.Identity;
|
||||||
using Baya.Domain.Entities.User;
|
using Baya.Domain.Entities.User;
|
||||||
using Baya.Infrastructure.Identity.Identity.Manager;
|
using Baya.Infrastructure.Identity.Identity.Manager;
|
||||||
|
using Baya.Infrastructure.Identity.Identity.PermissionManager;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
@@ -15,12 +18,25 @@ public class SeedDataBase : ISeedDataBase
|
|||||||
private readonly AppUserManager _userManager;
|
private readonly AppUserManager _userManager;
|
||||||
private readonly AppRoleManager _roleManager;
|
private readonly AppRoleManager _roleManager;
|
||||||
private readonly IConfiguration _configuration;
|
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<string, string[]> AdminSubRolePermissions =
|
||||||
|
new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[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;
|
_userManager = userManager;
|
||||||
_roleManager = roleManager;
|
_roleManager = roleManager;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_roleManagerService = roleManagerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Seed()
|
public async Task Seed()
|
||||||
@@ -38,9 +54,39 @@ public class SeedDataBase : ISeedDataBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await SeedAdminSubRolePermissionsAsync();
|
||||||
await SeedBootstrapAdminAsync();
|
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
|
// 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
|
// 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 —
|
// variables in a deployment). With neither configured — the default for Testing and any fresh boot —
|
||||||
|
|||||||
+54
@@ -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<Claim> { 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user