Files
clinicpro/docs/api/permission.md
T
hamedandClaude Opus 5 294ca19a46 fix(permissions): make the addresses resource real instead of decorative
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>
2026-08-07 19:16:02 +03:30

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)