14 KiB
رفع مشکلات دسترسی: پزشکِ عضو کلینیک + scope نماینده
پروژه
clinicpro (Backend auth/permission + Admin React SPA). کاملاً داخل همین پروژه است.
زمینه
دو نشتیِ دسترسی در پنل ادمین وجود دارد:
-
پزشکِ عضو کلینیک، دسترسی کاملِ مالک کلینیک میگیرد. وقتی یک پزشک از طریق دعوتنامه به کلینیک اضافه میشود، در
buildAvailableContextsبرایش یک context با'role' => 'clinic'ساخته میشود. چونswitchContextدر فرانتprimaryRole = context.roleمیگذارد، آن پزشک با سوییچ به این context عملاً «مالک کلینیک» میشود و به کل پنل کلینیک (پرسنل، مالی، خدمات، اشتراک، ویرایش کلینیک و...) دسترسی پیدا میکند. درست این است که او فقط «پزشکِ شاغل در آن کلینیک» باشد (نوبتهای خودش در آن کلینیک)، نه مدیر کلینیک. -
نماینده همهی پزشکان/کلینیکها را میبیند، نه فقط مالِ خودش. صفحات
DoctorsPage/ClinicsPageدر حالت نماینده از endpointهای عمومی/api/v1/doctorsو/api/v1/clinicsاستفاده میکنند که هیچ فیلتری رویrepresentation_idندارند. ضمناًRepresentationActionController::createClinicهنگام ساخت کلینیک،representation_idرا ست نمیکند (فقط createDoctor این کار را میکند). نتیجه: نماینده همهی دکترها/کلینیکهای سیستم را میبیند و کلینیکهای خودش هم تگگذاری نمیشوند.
مشکل / هدف
- پزشکِ عضو کلینیک نباید نقش/دسترسی مالک کلینیک بگیرد.
- نماینده فقط باید پزشکان و کلینیکهایی را ببیند که
representation_idآنها = id همان نماینده است (همانهایی که خودش ثبت کرده).
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/Auth/Controller/AuthController.php |
buildAvailableContexts() — ساخت context پزشکِ عضو کلینیک با role اشتباه clinic |
assets/admin/stores/authStore.ts |
switchContext → primaryRole = context.role؛ و type نقش |
assets/admin/components/layout/Sidebar.tsx |
منوی هر نقش (باید برای پزشکِ مهمانِ کلینیک محدود باشد) |
assets/admin/App.tsx |
RoleRoute صفحات کلینیک |
src/Representation/Controller/RepresentationActionController.php |
createClinic که representation_id ست نمیکند؛ مرجع createDoctor که میکند |
src/Doctor/Repository/DoctorRepository.php |
findWithFilters — فاقد فیلتر representation |
src/Clinic/Repository/ClinicRepository.php |
findWithFilters — فاقد فیلتر representation |
src/Doctor/Controller/DoctorController.php / src/Clinic/Controller/ClinicController.php |
endpointهای عمومی GET /api/v1/doctors و /api/v1/clinics |
assets/admin/pages/DoctorsPage.tsx / ClinicsPage.tsx |
لیست نماینده که از endpoint عمومی بدون scope استفاده میکند |
docs/api/auth.md, docs/api/doctor.md, docs/api/clinic.md, docs/api/representation.md |
مستندسازی |
وضعیت فعلی (کد واقعی)
buildAvailableContexts — context پزشکِ عضو کلینیک با role اشتباه:
if ($doctor = $this->doctorRepo->findByUser($user)) {
$contexts[] = [ 'type'=>'doctor', 'db_uuid'=>$doctor->getUuid(), 'name'=>'مطب شخصی '.$doctor->getName(), 'role'=>'doctor' ];
foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) {
$contexts[] = [
'type' => 'clinic',
'db_uuid' => $clinic->getUuid(),
'name' => $clinic->getName() ?? '',
'role' => 'clinic', // ← اشتباه: پزشکِ عضو، نقش مالک کلینیک میگیرد
'doctor_uuid' => $doctor->getUuid(),
];
}
}
// مالک واقعی کلینیک (این درست است — فقط وقتی clinic.user === خود کاربر)
if ($clinic = $this->clinicRepo->findByUser($user)) { ... 'role'=>'clinic' ... }
switchContext در authStore (نقش از context میآید):
primaryRole: res.data.context?.role ?? null,
RepresentationActionController::createClinic — بدون ست representation_id:
$clinic = new Clinic($ownerUser);
$clinic->setName($name);
// ... telephone/address/info
$this->em->persist($clinic); // ← representation_id ست نمیشود
(در مقابل، createDoctor این را دارد: $doctor->setRepresentationId($rep->getId());)
DoctorsPage/ClinicsPage (حالت نماینده، بدون scope):
const base = isRepresentation ? '/api/v1/doctors' : '/api/v1/admin/doctors'; // عمومی، بدون representation
const listBase = isRepresentation ? '/api/v1/clinics' : '/api/v1/admin/clinics'; // عمومی، بدون representation
وظایف
۱. نقشِ محدود برای پزشکِ عضو کلینیک (رفع نشتی دسترسی)
در buildAvailableContexts، context کلینیک برای پزشکِ عضو باید نقش مالک کلینیک ندهد. یک نقش/scope محدود بده، مثلاً role => 'doctor' با scope => 'clinic' (یعنی همان پزشک، ولی در محیط آن کلینیک — برای دیدن نوبتهای خودش در آن کلینیک):
foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) {
$contexts[] = [
'type' => 'clinic',
'db_uuid' => $clinic->getUuid(),
'name' => $clinic->getName() ?? '',
'role' => 'doctor', // پزشک میماند، نه مالک کلینیک
'scope' => 'clinic',
'doctor_uuid' => $doctor->getUuid(),
];
}
- بخش «صاحب کلینیک» (
$this->clinicRepo->findByUser($user)) باید دستنخورده بماند و همانrole => 'clinic'را داشته باشد — فقط مالک واقعی، نقش کامل کلینیک میگیرد. - نکتهی مهم Backend: هر endpointی که فرض میکند «کاربرِ با context کلینیک = مالک کلینیک» باید بازبینی شود تا با scope محدود سازگار باشد (دسترسی نوشتن روی پرسنل/خدمات/اشتراک/ویرایش کلینیک نباید برای پزشکِ مهمان باز باشد). مالکیت را با
clinic.getUser()->getId() === $user->getId()چک کن (الگوی موجود درClinicInvitationControllerخط ۲۰۰).
۲. Admin SPA — منو و مسیرهای محدود برای پزشکِ مهمانِ کلینیک
چون با اصلاح وظیفه ۱ نقش این کاربر در آن context doctor میشود (نه clinic)، Sidebar.tsx و RoleRouteهای App.tsx خودبهخود منوی پزشک را نشان میدهند و صفحات اختصاصی کلینیک (/admin/staff, /admin/my-financial با نقش clinic، ویرایش کلینیک، اشتراک) برای او باز نمیشوند. این را تأیید کن:
- در
App.tsxمطمئن شو صفحاتی مثلclinics/:uuid(ClinicDetailPage)،staff,subscriptionبرای نقشdoctorفقط در حدی باز است که منطقی است (مشاهده، نه مدیریت). اگر صفحهای باroles={['doctor','clinic']}هست که نباید برای پزشکِ مهمان نوشتنی باشد، در همان صفحه بر اساس مالکیت (context.role === 'clinic') دکمههای مدیریتی را پنهان کن. - اگر
scopeدر context هست، میتوان در فرانت ازcontext.scope === 'clinic'برای تمایز «پزشک در محیط کلینیک» استفاده کرد.
۳. نماینده — ستکردن representation_id روی کلینیک
در RepresentationActionController::createClinic، مثل createDoctor مالکیت نماینده را ست کن:
$clinic = new Clinic($ownerUser);
$clinic->setName($name);
// ...
$rep = $this->representationRepo->findByUser($user);
if ($rep !== null) {
$clinic->setRepresentationId($rep->getId());
}
$this->em->persist($clinic);
۴. فیلتر representation در لیست پزشکان و کلینیکها
در DoctorRepository::findWithFilters و ClinicRepository::findWithFilters یک فیلتر اختیاری representation اضافه کن:
// DoctorRepository
if (!empty($filters['representation'])) {
$qb->andWhere('d.representationId = :rep')->setParameter('rep', (int) $filters['representation']);
}
// ClinicRepository
if (!empty($filters['representation'])) {
$qb->andWhere('c.representationId = :rep')->setParameter('rep', (int) $filters['representation']);
}
- در controllerهای عمومی
GET /api/v1/doctorsوGET /api/v1/clinics، پارامترrepresentationاز query عبور داده شود بهfindWithFilters(همان الگوی بقیهی فیلترها). چون این پارامتر اختیاری است، رفتار عمومی سایت تغییری نمیکند.
هشدار امنیتی: نماینده نباید بتواند با دستکاری
representationدر query، لیست نمایندهی دیگری را ببیند. بهترین کار: یک endpoint اختصاصیِ scoped برای نماینده بساز که id را از کاربر جاری میگیرد (مثل/api/v1/representation/doctorsو/api/v1/representation/clinicsدر همانRepresentationActionControllerباfindByUser)، نه از query. این امنتر از پارامتر عمومی است. (پیشنهادِ ترجیحی.)
۵. Admin SPA — اتصال لیستهای نماینده به endpoint scoped
در DoctorsPage.tsx و ClinicsPage.tsx حالت نماینده را به endpoint scoped (وظیفه ۴) وصل کن:
// بهجای /api/v1/doctors عمومی:
const base = isRepresentation ? '/api/v1/representation/doctors' : '/api/v1/admin/doctors';
// بهجای /api/v1/clinics عمومی:
const listBase = isRepresentation ? '/api/v1/representation/clinics' : '/api/v1/admin/clinics';
- adapter نگاشت شکل پاسخ (که قبلاً برای DoctorsPage اضافه شده) را با شکل خروجی endpoint جدید هماهنگ کن. سادهترین کار: endpoint scoped همان شکل
/api/v1/admin/doctorsو/api/v1/admin/clinicsرا برگرداند تا نیازی به adapter نباشد.
۶. داشبورد نماینده — اعداد فقط از دامنهی خودش
مطمئن شو کارتها/آمار داشبورد نماینده (RepresentationDashboard در DashboardPage.tsx) از endpointهای نماینده میآیند که از قبل scoped هستند (/representation/{uuid}/dashboard/* و /representation/appointments). اگر شمارش «تعداد پزشکان من / کلینیکهای من» اضافه میشود، از همان endpoint scoped جدید بگیر (نه عمومی).
۷. مستندسازی
docs/api/auth.md: در توضیحavailable_contexts/primary_role، تفاوت «مالک کلینیک» (role: clinic) و «پزشکِ عضو در محیط کلینیک» (role: doctor,scope: clinic) را شرح بده.docs/api/doctor.mdوdocs/api/clinic.md: پارامتر/endpoint جدید فیلترrepresentation.docs/api/representation.md: endpointهای جدیدGET /api/v1/representation/doctorsو/clinics(اگر مسیر scoped انتخاب شد)؛ و اینکه createClinic حالاrepresentation_idست میکند.
نکات مهم
- امنیت اول: مالکیت همیشه از
#[CurrentUser]تعیین شود؛ هرگز بهrepresentationدر query برای scope اعتماد نکن (وظیفه ۴ پیشنهادِ endpoint scoped را ترجیح میدهد). Clinic.representationIdوDoctor.representationIdستون scalar هستند؛ فیلتر مستقیم روی همان ستون.- نقش
clinicدر سیستم = «مالک/مدیر کلینیک». پزشکِ عضو هرگز نباید این نقش را در context بگیرد. - تغییر
buildAvailableContextsرفتار سوییچ context را برای پزشکانِ چندکلینیکه عوض میکند؛ تست کن که مالک واقعی کلینیک هنوز نقش کامل دارد و پزشکِ مهمان فقط نوبتهای خودش را میبیند. - بعد از تغییر هر controller/مسیر،
cache:clearو بهروزرسانیdocs/api/*در همان session. - migration لازم نیست (هر دو ستون
representation_idاز قبل وجود دارند؛ این تسک فقط منطق/فیلتر/نقش است). - تستها (با
lexik:jwt:generate-token):- پزشکی که عضو یک کلینیک است →
available_contextsآن کلینیک بایدrole: doctor/scope: clinicباشد، نهclinic؛ و بعد از switch، منوی پزشک ببیند نه پنل کامل کلینیک. - مالک واقعی کلینیک → همچنان
role: clinicو دسترسی کامل. - نماینده →
GET /api/v1/representation/doctors|clinicsفقط ردیفهای باrepresentation_id = نمایندهی جاری؛ و کلینیکِ تازهساخته توسط نماینده بایدrepresentation_idداشته باشد. - نماینده نتواند با تغییر query، دادهی نمایندهی دیگر را ببیند.
- پزشکی که عضو یک کلینیک است →