A full role-by-role sweep (9 roles x 18 endpoints against the running app) showed
the addresses toggles in the owner's permission form controlled nothing. Grep
confirms it: no gate anywhere referenced 'addresses'. The panel's address list was
gated on appointment_settings.view instead — the same borrowed-permission pattern
already fixed for resources and treatment.
GET /api/v1/addresses now gates on addresses.view.
The resource drops to view-only. Creating, updating and deleting an address in
ClinicController is explicitly owner-or-admin
($clinic->getUser()->getId() !== $user->getId()), so those three actions could
never be delegated to a secretary or an invited doctor no matter what the form
said. Both role defaults narrow to ['view' => true] to match, and stored JSON
keeps its old keys harmlessly since merge only reads registry keys.
This widens secretary access: addresses.view defaults to true while
appointment_settings.view defaults to false, so secretaries who could not list
addresses now can. That is deliberate and costs no confidentiality — the same
addresses are already served anonymously from
GET /api/v1/clinic/{uuid}/addresses, which is whitelisted in security.yaml.
Verified live in three states: default 200, addresses.view off 403, and
addresses off with appointment_settings on still 403, proving the borrow is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
5.9 KiB
Markdown
119 lines
5.9 KiB
Markdown
# Permission Catalog API
|
|
|
|
> **Prefix:** `/api/v1/permission-catalog`
|
|
|
|
## چرا این اندپوینت هست
|
|
|
|
فهرستِ منابعِ قابلمجوزدهی تا پیش از این شش جا تکرار شده بود — دو Entity، سه فایل UI و
|
|
یک interface تایپاسکریپت — و از هم واگرا شده بودند. نتیجهاش این بود که صفحهٔ تازه
|
|
بهجای منبعِ خودش، مجوزِ نزدیکترین صفحهٔ موجود را قرض میگرفت.
|
|
|
|
حالا `App\Shared\Security\PermissionCatalog` تنها جایی است که میگوید چه منابع و
|
|
اکشنهایی وجود دارند. این اندپوینت همان رجیستری را به پنل میدهد تا هر دو فرمِ
|
|
مجوز — منشی و پزشکِ عضو کلینیک — بهجای فهرستِ هاردکد از آن رندر شوند.
|
|
|
|
**افزودن صفحهٔ تازه = یک ردیف در `PermissionCatalog::RESOURCES`.** فرانت تغییری لازم ندارد.
|
|
|
|
رجیستری فقط **ساختار** میدهد. مقدارِ پیشفرضِ هر نقش سیاست است و در Entity خودش
|
|
میماند: `DoctorSecretary::DEFAULT_PERMISSIONS` و `ClinicDoctorPermission::DEFAULT_PERMISSIONS`.
|
|
|
|
---
|
|
|
|
## GET `/api/v1/permission-catalog`
|
|
|
|
فهرستِ کاملِ منابع و اکشنها با برچسب فارسی.
|
|
|
|
**Permission:** `IS_AUTHENTICATED_FULLY`
|
|
|
|
پاسخ به کاربر بستگی ندارد و تا وقتی رجیستری عوض نشود ثابت است، پس کلاینت میتواند
|
|
بلندمدت cache کند (`staleTime: Infinity`). مقدارِ واقعیِ مجوزِ هر رابطه از
|
|
اندپوینتهای خودِ منشی/پزشک میآید، نه از اینجا.
|
|
|
|
`resources` آرایه است نه object، تا ترتیبِ نمایش بخشی از قرارداد باشد؛ ترتیبِ
|
|
کلیدهای JSON قرارداد نیست.
|
|
|
|
### Response `200` (خروجی واقعی، بریدهشده)
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"version": 1,
|
|
"resources": [
|
|
{
|
|
"key": "appointments",
|
|
"label": "مدیریت نوبتها",
|
|
"clinic_only": false,
|
|
"actions": [
|
|
{ "key": "view", "label": "مشاهده نوبتها" },
|
|
{ "key": "create", "label": "ایجاد نوبت" },
|
|
{ "key": "cancel", "label": "لغو نوبت" },
|
|
{ "key": "update_status", "label": "تغییر وضعیت نوبت" }
|
|
]
|
|
},
|
|
{
|
|
"key": "clinic_doctors",
|
|
"label": "مدیریت پزشکان کلینیک",
|
|
"clinic_only": true,
|
|
"actions": [
|
|
{ "key": "view", "label": "مشاهده پزشکان" },
|
|
{ "key": "create", "label": "افزودن پزشک" },
|
|
{ "key": "update", "label": "ویرایش پزشک" },
|
|
{ "key": "delete", "label": "حذف پزشک" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
| فیلد | نوع | توضیح |
|
|
| --- | --- | --- |
|
|
| `version` | int | نسخهٔ شِمای مجوزها. همان چیزی که در envelope ذخیره میشود |
|
|
| `resources[].key` | string | کلیدِ منبع — همان چیزی که در `permission` JSON مینشیند |
|
|
| `resources[].label` | string | برچسب فارسی برای نمایش |
|
|
| `resources[].clinic_only` | bool | فقط در محیطِ کلینیک معنا دارد؛ پزشکِ مستقل نباید ببیندش |
|
|
| `resources[].actions[].key` | string | `view` / `create` / `update` / `delete` / `cancel` / `update_status` |
|
|
| `resources[].actions[].label` | string | برچسب فارسیِ همان اکشن |
|
|
|
|
### منابعِ فعلی (۱۷)
|
|
|
|
`appointments` · `patients` · `treatment` · `payments` · `insurances` · `addresses` ·
|
|
`clinic_info` · `services` · `inventory` · `staff` · `tags` · `discounts` · `sms` ·
|
|
`appointment_settings` · `resources` · `clinic_doctors` · `subscription`
|
|
|
|
اکشنها یکسان نیستند: `subscription` فقط `view/create` دارد،
|
|
`clinic_info` / `appointment_settings` / `treatment` فقط `view/update`، و
|
|
`addresses` **فقط `view`**.
|
|
|
|
منبعِ `addresses` عمداً `create/update/delete` ندارد: ساخت و ویرایش و حذفِ آدرس در
|
|
`ClinicController` صریحاً owner-or-admin است
|
|
(`$clinic->getUser()->getId() !== $user->getId()`) و قابل واگذاری به منشی یا پزشکِ
|
|
عضو نیست. تا پیش از این هر سهٔ آن توگلها در فرمِ مالک بودند و هیچ چیزی را کنترل
|
|
نمیکردند.
|
|
|
|
### Errors
|
|
|
|
| Status | شرایط |
|
|
| --- | --- |
|
|
| `401` | بدون توکن یا توکنِ نامعتبر |
|
|
|
|
---
|
|
|
|
## سازگاری با دادهٔ موجود
|
|
|
|
`getPermissions()` روی هر دو Entity، JSONِ ذخیرهشده را روی پیشفرضِ نقش merge میکند:
|
|
|
|
- منبعی که در رجیستری هست و در JSONِ ردیف نیست (یعنی بعد از ساختِ آن ردیف اضافه شده)
|
|
**پیشفرضِ نقش** را میگیرد، نه `false`. بدون این، هر منبع تازه برای همهٔ ردیفهای
|
|
موجود خاموش میماند.
|
|
- مقدارِ صریحِ ذخیرهشده هرگز بازنویسی نمیشود — حتی `false`.
|
|
- هیچ migration دادهٔ انبوهی لازم نیست؛ ستون `permission` از نوع `json` است و
|
|
schema عوض نمیشود.
|
|
|
|
نوشتنها از `PermissionCatalog::filterPatch()` عبور میکنند: منبع یا اکشنِ خارج از
|
|
رجیستری **بیصدا** کنار گذاشته میشود (نه `422`) — همان رفتاری که کلاینتهای فعلی روی
|
|
آن حساب کردهاند. بقیهٔ کلیدهای همان درخواست اعمال میشوند.
|
|
|
|
مصرفکنندهها: [secretary.md](secretary.md) · [clinic.md](clinic.md)
|