feat(clinic-doctor): full permission coverage + enforcement, parity with secretary
The clinic-member-doctor permission system (ClinicDoctorPermission) lagged the secretary system: only 6 resources, enforced in ~6 places, dead toggles (services.update never checked), and a sidebar showing just appointments+patients. Bring it to parity so a clinic owner can control exactly what each member doctor does — while an independent doctor stays completely unrestricted. Coverage: add insurances, addresses, inventory, tags, staff, discounts, sms to ClinicDoctorPermission::DEFAULT_PERMISSIONS + DoctorPermissionsModal (subscription/clinic_doctors stay owner-only by design). New App\Clinic\Security\ClinicDoctorAccessChecker (parallel to SecretaryAccessChecker): - denyUnlessGranted(user, resource, action): 403 only for a clinic-member doctor in the clinic context; owner/admin/secretary/independent-doctor pass through. - memberClinicId(user): resolves the member doctor to the CLINIC's tenant so the role-based controllers (Inventory/Tag/Staff/Discount/Sms) stop showing them their personal tenant in clinic context. Enforcement wired into 10 controllers alongside the existing secretary gates: ClinicService (services), Insurance (insurances), Patient (patients+payments), Staff, Discount, Inventory, Tag, SmsWallet, Payment, PaymentMethod. Frontend: the guest-doctor sidebar branch now exposes every permitted resource (gated by can()) plus a «تنظیمات» entry; both settings navs (PurchaseSubscription Sidebar + SETTINGS_MENU) are now permission-filtered for a scope=clinic doctor, not just secretaries; my-payments route gets the missing payments permission. CRUD-button gating already applies (usePermissions is role-agnostic). Tests: ClinicDoctorPermissionEnforcementTest (member denied/allowed + independent-doctor-unrestricted); guest-doctor sidebar gating. Backend 375 pass, frontend 503 pass. docs/api/clinic.md updated with the full resource set + enforcement notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
-7
@@ -382,19 +382,28 @@ The envelope is always returned in full (`{version, resources}`); it is never fl
|
||||
{
|
||||
"version": 1,
|
||||
"resources": {
|
||||
"appointments": { "view": true, "create": true, "cancel": true, "update_status": true },
|
||||
"appointment_settings": { "view": true, "update": true },
|
||||
"patients": { "view": true, "create": true, "update": true, "delete": false },
|
||||
"payments": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"services": { "view": true, "update": false },
|
||||
"clinic_info": { "view": true, "update": false }
|
||||
"appointments": { "view": true, "create": true, "cancel": true, "update_status": true },
|
||||
"appointment_settings": { "view": true, "update": true },
|
||||
"patients": { "view": true, "create": true, "update": true, "delete": false },
|
||||
"payments": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"services": { "view": true, "update": false },
|
||||
"clinic_info": { "view": true, "update": false },
|
||||
"insurances": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"addresses": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"inventory": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"tags": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"staff": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"discounts": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"sms": { "view": false, "create": false, "update": false, "delete": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`active: false` revokes everything at once regardless of the individual flags. The clinic owner and `ROLE_ADMIN` bypass all checks and can never be locked out.
|
||||
|
||||
Unknown resources and unknown actions in a PATCH body are silently ignored, so a client cannot invent permission keys.
|
||||
Unknown resources and unknown actions in a PATCH body are silently ignored, so a client cannot invent permission keys. `subscription` و `clinic_doctors` عمداً منبع نیستند — عملیاتِ مالکِ کلینیکاند، نه پزشکِ عضو.
|
||||
|
||||
**اعمال (enforcement):** همهٔ منابع در بکاند enforce میشوند. نقطهٔ واحد `App\Clinic\Security\ClinicDoctorAccessChecker` (`denyUnlessGranted` / `memberClinicId`) که **فقط پزشکِ عضوِ کلینیک در محیطِ فعالِ کلینیک** را محدود میکند؛ مالک/ادمین/منشی/پزشکِ مطبِ شخصی دستنخورده عبور میکنند. کنترلرهایی که tenant را نقشمحور حل میکنند (Inventory/Tag/Staff/Discount/Sms) با `memberClinicId` پزشکِ عضو را به دادهٔ کلینیک میبرند (نه مطبِ شخصی). نبودِ مجوز → `403`. در پنل، سایدبار/Route/دکمههای CRUD با `usePermissions().can` برای محیطِ `scope=clinic` گِیت میشوند.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user