feat: add per-doctor permissions management in clinics

- Implement DoctorPermissionsModal for managing doctor permissions in clinics.
- Create usePermissions hook to handle user permissions context.
- Add migration for clinic_doctor_permissions table with default permissions.
- Develop ClinicDoctorPermissionController for handling permissions API.
- Create ClinicDoctorPermission entity to manage permissions data.
- Implement ClinicDoctorPermissionRepository for database interactions.
- Add ClinicDoctorPermissionChecker for permission validation logic.
- Write tests for clinic doctor permissions functionality.
This commit is contained in:
hamed
2026-07-18 09:44:13 +03:30
parent 3a23aa242e
commit e4ddd38f0c
17 changed files with 1504 additions and 44 deletions
+26 -2
View File
@@ -287,8 +287,20 @@ Authorization: Bearer <token>
"type": "clinic",
"db_uuid": "clinic-uuid-...",
"name": "کلینیک سلامت",
"role": "clinic",
"doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966"
"role": "doctor",
"scope": "clinic",
"doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
"permissions": {
"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 }
}
}
}
]
}
@@ -304,6 +316,18 @@ Authorization: Bearer <token>
| `context` | object\|null | context فعال انتخاب‌شده |
| `available_contexts` | array | همه محیط‌های کاری قابل انتخاب |
**فیلد `permissions` در هر context:**
| حالت context | مقدار `permissions` |
|---|---|
| مطب شخصی پزشک (`type: doctor`، `role: doctor`) | `null` — محیط خودش، محدودیتی ندارد |
| مالک کلینیک (`role: clinic`) | `null` — مالک هرگز محدود نمی‌شود |
| پزشکِ عضو کلینیک (`role: doctor`، `scope: clinic`) | envelope کامل `{version, resources}` از `clinic_doctor_permissions` |
| پزشکِ عضوی که دسترسی‌اش غیرفعال شده | `{version: 1, resources: {}}` — یعنی هیچ دسترسی |
| منشی (`role: secretary`) | envelope کامل از `doctor_secretaries` |
نکتهٔ مهم برای کلاینت: **نبودِ `permissions` (یا `null`) یعنی «بدون محدودیت»، نه «بدون دسترسی».** ساختار و کلیدهای مجوز پزشکِ عضو کلینیک در `docs/api/clinic.md` → بخش *Clinic Doctor Permissions* آمده است.
**قانون `primary_role`** (اولویت‌بندی):
- `ROLE_ADMIN``"admin"`
- `ROLE_CLINIC``"clinic"`
+118 -2
View File
@@ -159,7 +159,7 @@ Get clinic detail.
Update a clinic.
**Permission:** `AUTH` must be the clinic owner or `ROLE_ADMIN`
**Permission:** `AUTH` — the clinic owner, `ROLE_ADMIN`, or a member doctor holding `clinic_info.update` (see **Clinic Doctor Permissions**)
### Path Parameters
| Param | Type | Description |
@@ -314,7 +314,7 @@ Get doctors associated with a clinic.
## DELETE `/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}`
Detach a doctor from a clinic. This removes the clinic↔doctor link only (the `clinic_doctors` association); it does **not** delete the doctor or change the doctor's own `active` appointment flag.
Detach a doctor from a clinic. This removes the clinic↔doctor link (the `clinic_doctors` association) and the doctor's `clinic_doctor_permissions` row; it does **not** delete the doctor or change the doctor's own `active` appointment flag.
**Permission:** `AUTH` — the caller must be `ROLE_ADMIN` **or** the owner of this clinic (`ROLE_CLINIC` whose user owns `clinicUuid`). Any other authenticated user gets `403`.
@@ -338,6 +338,122 @@ Detach a doctor from a clinic. This removes the clinic↔doctor link only (the `
---
## Clinic Doctor Permissions
Each doctor attached to a clinic has a permission envelope scoped to **that clinic only** — the doctor's own practice is never affected. Rows live in `clinic_doctor_permissions` (one per clinic+doctor) and are created lazily with defaults for doctors who joined before this feature existed.
The envelope is always returned in full (`{version, resources}`); it is never flattened.
```json
{
"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 }
}
}
```
`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.
---
## GET `/api/v1/admin/clinic/{clinicUuid}/doctor-permissions`
List the permission rows of every doctor in the clinic.
**Permission:** `AUTH` — clinic owner or `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "ce200cde-826d-11f1-b923-c282b864cdcc",
"clinic_uuid": "41e325c4-e825-4067-8438-5d828ecaee09",
"doctor_uuid": "bcabb3a8-cae3-45ec-876c-548f9c1e1569",
"doctor_name": "دکتر تست",
"active": true,
"permissions": { "version": 1, "resources": { "...": {} } },
"created_at": 1784352916,
"updated_at": 1784352916
}
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_ACCESS_DENIED` | 403 | Neither admin nor the clinic owner |
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
---
## GET `/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}/permissions`
Read one doctor's permissions. Creates the row with defaults if it does not exist yet.
**Permission:** `AUTH` — clinic owner or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `clinicUuid` | string (UUID) | Clinic UUID |
| `doctorUuid` | string (UUID) | Doctor UUID — must already be attached to this clinic |
### Response `200`
Single permission object (same shape as one item of the list above).
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_ACCESS_DENIED` | 403 | Neither admin nor the clinic owner |
| `ERR_NOT_FOUND_001` | 404 | Clinic not found, or doctor not attached to this clinic |
---
## PATCH `/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}/permissions`
Update one doctor's permissions. **Deep merge** — only the resources/actions present in the body change; everything else keeps its current value.
**Permission:** `AUTH` — clinic owner or `ROLE_ADMIN`
### Request Body
```json
{
"permissions": { "resources": { "payments": { "create": true } } },
"active": true
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `permissions` | object | ❌ | `{resources: {<resource>: {<action>: bool}}}`. The bare `{<resource>: {...}}` form is also accepted. |
| `active` | bool | ❌ | `false` revokes all access to this clinic |
### Response `200`
Updated permission object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_ACCESS_DENIED` | 403 | Neither admin nor the clinic owner |
| `ERR_NOT_FOUND_001` | 404 | Clinic not found, or doctor not attached to this clinic |
| `ERR_VALIDATION_001` | 422 | `permissions` is not an object |
---
## POST `/file/upload/clinic_pro/clinic/field_clinic_logo`
Upload clinic logo.