Files
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

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