Files
clinicpro/.claude/prompt/fix-guest-doctor-clinic-context-access.md

9.3 KiB
Raw Permalink Blame History

رفع باگ دسترسی کاملِ پزشکِ مهمان در محیط کلینیک

پروژه

clinicpro (Admin React SPA + گارد Backend). ادامه/تکمیلِ پرامپت قبلی fix-access-scoping-doctor-clinic-representation.md.

زمینه

در پرامپت قبلی، نقشِ context پزشکی که به کلینیک دعوت می‌شود از clinic به doctor + scope: clinic تغییر کرد (در buildAvailableContexts). این درست بود ولی ناقص: هیچ‌جای فرانت و بک‌اند به فیلد scope توجه نمی‌کند. در نتیجه وقتی پزشکِ مهمان به محیط کاریِ آن کلینیک سوییچ می‌کند (db_uuid = clinicUuid)، همان منوی کاملِ نقش doctor را می‌بیند — شامل پرسنل، منشی‌ها، سرویس‌ها، اشتراک، پرونده بیماران — و عملاً به امکانات مدیریتیِ محیط کلینیک دسترسی دارد. این یعنی باگ «پزشکِ اضافه‌شده به کلینیک، دسترسی به همه‌ی امکانات کلینیک دارد» هنوز باقی است.

رفتار درست: پزشکِ مهمان در محیط کلینیک فقط باید نوبت‌های خودش در آن کلینیک را ببیند (read/منظوره‌ی محدود)، نه ابزارهای مدیریت کلینیک. در محیط «مطب شخصی» خودش (context با scope خالی) همه‌ی ابزارها در دسترس می‌ماند.

مشکل / هدف

scope: 'clinic' که روی context پزشکِ مهمان ست شده، باید واقعاً دسترسی را محدود کند — هم در منوی Sidebar، هم در RoleRouteهای SPA، و هم با گارد در endpointهای مدیریتی (defense in depth).

فایل‌های مرتبط

فایل نقش
src/Auth/Controller/AuthController.php buildAvailableContexts (از قبل scope: 'clinic' می‌دهد) و userinfo/switchContext که context را برمی‌گرداند
assets/admin/stores/authStore.ts نگه‌داری context فعال + primaryRole؛ باید scope در دسترس باشد
assets/admin/components/layout/Sidebar.tsx منوی نقش doctor — باید در scope: clinic محدود شود
assets/admin/App.tsx RoleRoute — صفحات مدیریتیِ پزشک باید در scope: clinic مسدود شوند
src/Staff/Controller/StaffController.php resolveEntity پزشک = scope شخصی؛ گارد لازم برای حالت مهمان
src/Secretary/Controller/SecretaryController.php همان منطق برای منشی‌ها
src/ClinicService/Controller/* (سرویس‌ها) همان منطق
docs/api/auth.md مستندسازی معنای scope

وضعیت فعلی (کد واقعی)

buildAvailableContexts — context مهمان دارای scope ولی بدون اثر:

foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) {
    $isOwner = $clinic->getUser()->getId() === $user->getId();
    $contexts[] = [
        'type'        => 'clinic',
        'db_uuid'     => $clinic->getUuid(),
        'name'        => $clinic->getName() ?? '',
        'role'        => $isOwner ? 'clinic' : 'doctor',
        'scope'       => $isOwner ? null : 'clinic',   // ← ست می‌شود ولی هیچ‌جا مصرف نمی‌شود
        'doctor_uuid' => $doctor->getUuid(),
    ];
}

Sidebar.tsx — منوی پزشک، بدون توجه به scope (همه‌ی آیتم‌های مدیریتی نمایش داده می‌شوند):

if (primaryRole === "doctor") {
    return [
        { label: "عمومی",  items: [{ to: "/admin/dashboard", ... }] },
        { label: "پروفایل", items: [{ to: "/admin/profile", ... }] },
        { label: "مدیریت", items: [
            { to: "/admin/appointments",   label: "نوبت‌های من" },
            { to: "/admin/my-patients",    label: "پرونده بیماران" },
            { to: "/admin/staff",          label: "پرسنل" },        // ← نباید برای مهمان باشد
            { to: "/admin/my-secretaries", label: "منشی ها" },      // ← نباید
            { to: "/admin/clinic-services",label: "سرویس‌ها" },      // ← نباید
            ...
        ]},
    ];
}

authStore.ts — context فعال نگه‌داری می‌شود (شامل role و باید scope):

primaryRole: res.data.context?.role ?? null,
context:     res.data.context,

(نوع ContextItem احتمالاً scope? ندارد — باید اضافه شود.)

وظایف

۱. در دسترس‌بودن scope در authStore

  • در assets/admin/stores/authStore.ts، type ContextItem فیلد scope?: string | null را داشته باشد و از userinfo/switchContext پر شود.
  • یک selector/مقدار مشتق در store یا کامپوننت‌ها: isClinicScopedDoctor = primaryRole === 'doctor' && context?.scope === 'clinic'.

۲. محدودکردن منوی Sidebar برای پزشکِ مهمان

در Sidebar.tsx، شاخه‌ی primaryRole === "doctor":

  • اگر context?.scope === 'clinic' بود، فقط آیتم‌های مجاز را برگردان:
    • داشبورد (/admin/dashboard)
    • نوبت‌ها (/admin/appointments) — نوبت‌های پزشک در آن کلینیک
  • آیتم‌های staff, my-secretaries, clinic-services, subscription, my-patients, profile (مدیریت/داده‌ی شخصیِ مطب) را در این حالت نشان نده.
  • در محیط «مطب شخصی» (scope خالی) منوی کامل فعلی بماند.

۳. گارد RoleRoute در SPA برای صفحات مدیریتیِ مهمان

در App.tsx:

  • یک گارد سبک اضافه کن (مثلاً گسترش RoleRoute یا یک wrapper ClinicScopeBlock) که اگر کاربر doctor با context.scope === 'clinic' است، مسیرهای staff, my-secretaries, clinic-services, subscription, my-patients, profile را به /admin/dashboard ریدایرکت کند.
  • مسیرهای dashboard و appointments باز بمانند.

۴. گارد Backend (defense in depth)

حتی با محدودیت UI، endpointهای مدیریتی نباید به پزشکِ مهمان اجازه‌ی عمل روی کلینیک بدهند. الگوی موجود این است که StaffController::resolveEntity برای ROLE_DOCTOR همیشه scope شخصیِ پزشک را برمی‌گرداند (['doctor', doctorId]) — یعنی پزشک فقط پرسنل/سرویسِ مطب خودش را می‌بیند، نه کلینیک. این را تأیید کن و مطمئن شو هیچ endpoint مدیریتی، db_uuid/clinicUuid را از کلاینت گرفته و بدون چک مالکیت (clinic.getUser() === currentUser) روی کلینیک عمل نمی‌کند:

  • StaffController, SecretaryController, ClinicService controllers, و ClinicController (update/address/logo) را مرور کن؛ هر مسیری که کلینیک را با findByUuid($clinicUuid) می‌گیرد باید مالکیت یا ROLE_ADMIN را چک کند (الگوی $clinic->getUser()->getId() !== $user->getId() && !hasRole('ROLE_ADMIN') → 403). اگر جایی این چک نیست، اضافه کن.
  • نتیجه‌ی مورد انتظار: پزشکِ مهمان (نه مالک، نه ادمین) روی این endpointها 403 بگیرد.

۵. مستندسازی

  • docs/api/auth.md: در توضیح context/available_contexts، معنای scope: 'clinic' را شرح بده: «پزشکِ مهمان در محیط کلینیک؛ فقط نوبت‌های خودش، بدون دسترسی مدیریتی به کلینیک».

نکات مهم

  • منبع حقیقتِ scope = context فعال، نه dbUuid تنها. تمایز «مالک کلینیک» (role: clinic)، «پزشک در مطب شخصی» (role: doctor, بدون scope)، و «پزشک مهمان در کلینیک» (role: doctor, scope: clinic).
  • در محیط مطب شخصی هیچ محدودیتی اضافه نشود؛ فقط scope: clinic محدود می‌شود.
  • گارد UI برای UX است؛ گارد واقعی Backend است — هر دو لازم‌اند (وظیفه ۴ نباید رها شود).
  • ادمین (ROLE_ADMIN) همیشه دسترسی کامل دارد؛ مالک کلینیک (role: clinic) هم بدون تغییر.
  • بعد از تغییر backend، cache:clear و به‌روزرسانی docs/api/auth.md. تغییر فرانت با tsc --noEmit + yarn dev تست شود.
  • تست‌ها (با lexik:jwt:generate-token):
    • پزشکِ مهمان → userinfo آن کلینیک role:doctor, scope:clinic؛ بعد از switch، Sidebar فقط «داشبورد» و «نوبت‌ها»؛ مسیرهای /admin/staff و /admin/clinic-services ریدایرکت به داشبورد.
    • همان پزشک روی endpoint مدیریتی کلینیک (مثلاً ساخت پرسنل برای آن کلینیک) → 403.
    • مالک واقعی کلینیک → دسترسی کامل بدون تغییر.
    • پزشک در مطب شخصی → منوی کامل و دسترسی به staff/services خودش بدون تغییر.