feat: Implement secretary permissions enforcement across multiple resources
- Added SecretaryAccessChecker to manage resource access for secretaries. - Integrated permission checks for payments, inventory, and tags in relevant controllers. - Updated PaymentController and PaymentMethodController to enforce secretary permissions. - Enhanced TenantTagController to check permissions for tag management actions. - Introduced tests for secretary resource enforcement, ensuring proper access control. - Updated DoctorSecretary entity to include inventory and tags permissions. - Created a comprehensive audit document for secretary permissions coverage and enforcement. - Fixed potential crashes in SecretaryDashboard when rendering without doctor data.
This commit is contained in:
+27
-25
@@ -196,41 +196,43 @@ Returns stats for the authenticated secretary and (conditionally) today's appoin
|
||||
|
||||
### Response `200`
|
||||
|
||||
پاسخ بر اساس `scope` محیطِ فعالِ منشی دو شکل دارد. **کلید `scope` تمایزدهنده است**: در `scope=doctor` فیلد `doctor` هست (نه `clinic`) و در `scope=clinic` فیلد `clinic` (نه `doctor`). کلاینت باید هر دو را مدیریت کند و بهصورت مستقیم به `data.doctor.name` دسترسی نگیرد.
|
||||
|
||||
**منشیِ مطبِ شخصی (`scope=doctor`):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"doctor": {
|
||||
"uuid": "string",
|
||||
"name": "string",
|
||||
"degree": "string | null"
|
||||
},
|
||||
"permissions": {
|
||||
"resources": {
|
||||
"appointments": {
|
||||
"view": true,
|
||||
"edit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"today_appointments": 8,
|
||||
"tomorrow_appointments": 5
|
||||
},
|
||||
"scope": "doctor",
|
||||
"doctor": { "uuid": "string", "name": "string", "degree": "string | null" },
|
||||
"permissions": { "resources": { "appointments": { "view": true, "update_status": false } } },
|
||||
"stats": { "today_appointments": 8, "tomorrow_appointments": 5 },
|
||||
"today_appointments": [
|
||||
{
|
||||
"uuid": "string",
|
||||
"patient_name": "string | null",
|
||||
"patient_mobile": "string",
|
||||
"slot_start": 1700000000,
|
||||
"status": "reserved"
|
||||
}
|
||||
{ "uuid": "string", "patient_name": "string | null", "patient_mobile": "string", "slot_start": 1700000000, "status": "reserved" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; up to 10 records when visible.
|
||||
**منشیِ کلینیک (`scope=clinic`) — بدون کلید `doctor`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"scope": "clinic",
|
||||
"clinic": { "uuid": "string", "name": "string" },
|
||||
"permissions": { "resources": { "appointments": { "view": true } } },
|
||||
"stats": { "today_appointments": 12, "tomorrow_appointments": 7 },
|
||||
"today_appointments": [
|
||||
{ "uuid": "string", "patient_name": "string | null", "patient_mobile": "string", "slot_start": 1700000000, "status": "reserved", "doctor_name": "string" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; up to 10 records (doctor scope) / 20 (clinic scope) when visible. در scope کلینیک هر ردیف `doctor_name` هم دارد.
|
||||
|
||||
### Errors
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> **Prefix:** `/api/v1/insurances`, `/api/v1/insurance`, `/api/v1/admin/insurance`
|
||||
|
||||
> **دسترسی منشی:** endpointهای غیرادمینِ بیمه (insurance-pricing, billing/tenant-insurances, service-coverage, `/api/v1/insurance/*`) برای `ROLE_SECRETARY` روی منبع `insurances` اعمال میشوند (`SecretaryAccessChecker`): GET→`view`, POST→`create`, PATCH/PUT→`update`, DELETE→`delete`؛ نبودِ مجوز → `403`. جزئیات: [secretary.md](secretary.md).
|
||||
|
||||
Two resource types:
|
||||
1. **Insurance** — master list of insurance companies managed by admin
|
||||
2. **DoctorInsurance** — a doctor's acceptance of a specific insurance (with optional price)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> **Prefix:** `/api/v1/inventory-*`
|
||||
|
||||
> **دسترسی منشی:** برای `ROLE_SECRETARY` روی منبع `inventory` اعمال میشود (`SecretaryAccessChecker`): GET→`view`, POST→`create`, PATCH→`update`, DELETE→`delete`؛ نبودِ مجوز → `403`. پیشفرضِ منشی برای این منبع همه `false` است. جزئیات: [secretary.md](secretary.md).
|
||||
|
||||
Per-tenant (doctor/clinic) consumable-stock management: **items** and **packages**
|
||||
(bundles of items). Every row is scoped to the caller's resolved entity
|
||||
(`doctor` / `clinic`), exactly like Tenant Tags — a tenant only ever sees and
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
Patient records track patients per entity (doctor or clinic). Each record holds multiple sessions (visits). Access requires an active subscription with the `patient_records` feature.
|
||||
|
||||
> **دسترسی منشی:** برای `ROLE_SECRETARY` روی منبع `patients` اعمال میشود (`SecretaryAccessChecker`). خواندنها از طریق `scope()` کنترل میشوند: منشیِ بدون `patients.view` هیچ پروندهای نمیبیند (scope = unknown → 404/403). نوشتنها guard جداگانه دارند: ایجاد بیمار→`patients.create`؛ ویرایش/زیرمنابع (note/call/message/medical-record/attachment/session)→`patients.update`. عملیاتِ مالیِ بیمار (کیفپول، پرداختِ جلسه) روی منبع `payments` اعمال میشوند. نبودِ مجوز → `403`. جزئیات: [secretary.md](secretary.md).
|
||||
|
||||
**Base path:** `/api/v1`
|
||||
**Auth:** Bearer JWT (doctor, clinic, or secretary)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
Per-clinic payment methods managed from the settings screen (`/admin/my-financial`,
|
||||
tab "مدیریت پرداخت"). Two resources: **bank accounts** and **POS (card reader) devices**.
|
||||
|
||||
> **دسترسی منشی:** روشهای پرداخت زیرمجموعهٔ منبع `payments` هستند. برای `ROLE_SECRETARY` (`SecretaryAccessChecker`): GET→`payments.view`, POST→`payments.create`, PUT/PATCH→`payments.update`؛ نبودِ مجوز یا رابطهٔ فعال → `403`. جزئیات: [secretary.md](secretary.md).
|
||||
Records are stored so a patient invoice can later reference which account/device a
|
||||
service payment was made to.
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
> **Prefix:** `/api/v1/payment`, `/api/v1/subscription-payment`
|
||||
> **Supported Gateways:** `mellat` (Mellat Bank SOAP) | `sep` (SEP REST)
|
||||
|
||||
> **دسترسی منشی:** `GET /api/v1/my/payments` برای `ROLE_SECRETARY` به مجوز `payments.view` نیاز دارد (`SecretaryAccessChecker`)؛ نبودِ مجوز → `403`. جزئیات: [secretary.md](secretary.md).
|
||||
|
||||
---
|
||||
|
||||
## معماری (Flow & مسئولیتها)
|
||||
|
||||
+39
-2
@@ -69,7 +69,19 @@ Create a secretary for a doctor.
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"clinic_info": { "view": true, "update": false }
|
||||
"clinic_info": { "view": true, "update": false },
|
||||
"inventory": {
|
||||
"view": false,
|
||||
"create": false,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"tags": {
|
||||
"view": false,
|
||||
"create": false,
|
||||
"update": false,
|
||||
"delete": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +119,20 @@ Create a secretary for a doctor.
|
||||
|
||||
**Permissions Structure:**
|
||||
|
||||
مجموعهٔ منابع (resources) بر اساس صفحات موجود پنل ادمین است. `mergePermissions` هر منبع/اکشن ارسالشده را deep-merge میکند؛ فقط `appointments` در بکاند enforce میشود (`MyAppointmentsController`, `DashboardController`)، بقیه UI/ذخیرهای هستند.
|
||||
مجموعهٔ منابع (resources) بر اساس صفحات و ماژولهای در دسترسِ منشی است: `appointments`, `patients`, `payments`, `insurances`, `addresses`, `clinic_info`, `inventory`, `tags`. `mergePermissions` هر منبع/اکشن ارسالشده را deep-merge میکند. منابع `inventory` و `tags` بهصورت پیشفرض همه `false`اند (default-deny)؛ بقیه طبق `DEFAULT_PERMISSIONS`.
|
||||
|
||||
**اعمال (enforcement):** همهٔ منابع در بکاند enforce میشوند، نه فقط `appointments`. منبعِ حقیقت، ستون JSON `permission` روی ردیفِ فعالِ `DoctorSecretary` در محیطِ فعالِ کاربر (`UserActiveContext.db_uuid`) است؛ نقطهٔ مرکزی `App\Secretary\Security\SecretaryAccessChecker` (`can` / `canOrNonSecretary` / `denyUnlessGranted`). نبودِ مجوز → `403 ERR_FORBIDDEN_001`. نقشه:
|
||||
|
||||
| Resource | Enforced in | Action → endpoint |
|
||||
| --- | --- | --- |
|
||||
| `appointments` | `AppointmentAccessChecker`, `MyAppointmentsController`, `DashboardController` | view/create/cancel/update_status |
|
||||
| `patients` | `PatientController` (خواندنها via `scope()` → بدون `view` هیچ پروندهای؛ نوشتنها با guard) | view/create/update/delete |
|
||||
| `payments` | `PaymentController::myPayments`, `PaymentMethodController` (bank/pos), `PatientController` (کیفپول + پرداختِ جلسه) | view/create/update/delete |
|
||||
| `insurances` | `InsuranceController` (insurance-pricing, tenant-insurances, service-coverage, doctor-insurance) | view/create/update/delete |
|
||||
| `inventory` | `InventoryController` (items + packages) | view/create/update/delete |
|
||||
| `tags` | `TenantTagController` (لیست با `tags.view` یا `patients.view`؛ نوشتنها با `tags.*`) | view/create/update/delete |
|
||||
|
||||
نقشهای غیرمنشی (`ROLE_CLINIC`/`ROLE_DOCTOR`/`ROLE_ADMIN`) از این چک عبور میکنند (`canOrNonSecretary` برایشان `true`). منشیِ بدون رابطهٔ فعال/context هیچ مجوزی ندارد → همهچیز `403`.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -146,6 +171,18 @@ Create a secretary for a doctor.
|
||||
"clinic_info": {
|
||||
"view": true,
|
||||
"update": false
|
||||
},
|
||||
"inventory": {
|
||||
"view": false, // انبار: مشاهده
|
||||
"create": false, // ایجاد کالا/بسته
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"tags": {
|
||||
"view": false, // تگها؛ لیست با tags.view یا patients.view
|
||||
"create": false,
|
||||
"update": false,
|
||||
"delete": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user