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>
5.9 KiB
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 (خروجی واقعی، بریدهشده)
{
"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 · clinic.md