# دسترسی نقش نماینده (ROLE_REPRESENTATION) به پنل ادمین ## پروژه `clinicpro` (Backend + Admin React SPA). کاملاً داخل همین پروژه است؛ پرامپت همتای frontend عمومی لازم نیست. ## زمینه کاربرانِ دارای نقش `ROLE_REPRESENTATION` (نماینده‌ی فروش/شهر) باید بتوانند وارد پنل ادمین (`https://clinic-pro.ddev.site/admin/dashboard`) شوند و کارهای محدودِ خودشان را انجام دهند: 1. **افزودن پزشک** 2. **افزودن کلینیک** 3. **دیدن نوبت‌های پزشکانی که زیرمجموعه‌ی همان نماینده‌اند** (پزشکانی که `Doctor.representationId` = id همان نماینده است) 4. **داشبورد اختصاصی خودشان** (درآمد/کمیسیون ماهانه و سالانه که از قبل موجود است) الان این نقش عملاً به پنل راه ندارد و حتی اگر داشته باشد همه‌چیز مسدود است. شکاف‌های دقیق: - `App\Auth\Controller\AuthController::resolvePrimaryRole()` هیچ شاخه‌ای برای `ROLE_REPRESENTATION` ندارد → چنین کاربری `primary_role: 'user'` می‌گیرد و پنل او را نمی‌شناسد. - `App\Admin\Controller\AdminApiController` در سطح کلاس `#[IsGranted('ROLE_ADMIN')]` است → endpointهای `createDoctor` (`POST /api/v1/admin/doctors`) و `createClinic` (`POST /api/v1/admin/clinic`) برای نماینده مسدودند. - Admin SPA: `assets/admin/App.tsx` تابع `RoleRoute` فقط `['admin']` را برای کلینیک‌ها/پزشکان می‌پذیرد؛ `Sidebar.tsx` فقط برای `admin|clinic|doctor|secretary` منو دارد (نماینده ندارد)؛ `DashboardPage.tsx` فقط endpointهای `/api/v1/admin/dashboard/*` را صدا می‌زند (admin-only). - هیچ endpointی برای «نوبت‌های پزشکانِ یک نماینده» وجود ندارد. ## مشکل / هدف به نقش `ROLE_REPRESENTATION` اجازه‌ی ورود به پنل و دسترسی به ۴ قابلیت بالا داده شود — بدون اینکه دسترسی‌های ادمین (کاربران، پرداخت‌ها، تسویه، بلاگ و...) برای او باز شود. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `src/Auth/Controller/AuthController.php` | `resolvePrimaryRole()` و gate لاگین staff (`/oauth/userinfo` → `primary_role`) | | `src/Admin/Controller/AdminApiController.php` | کلاس `#[IsGranted('ROLE_ADMIN')]`؛ متدهای `createDoctor`, `createClinic`, `appointments` | | `src/Representation/Controller/RepresentationController.php` | الگوی permission نماینده (`$rep->getUser()->getId() === $user->getId()`)؛ dashboard ماهانه/سالانه | | `src/Representation/Repository/RepresentationRepository.php` | یافتن نماینده از روی user (`findByUser`) | | `src/Doctor/Entity/Doctor.php` | `representationId` (FK به نماینده) — مبنای «پزشکان این نماینده» | | `src/Appointment/...` (Repository/Controller) | منبع لیست نوبت‌ها برای endpoint جدید | | `assets/admin/App.tsx` | `RoleRoute` + ثبت routeهای جدید | | `assets/admin/components/layout/Sidebar.tsx` | منوی نقش `representation` | | `assets/admin/pages/DashboardPage.tsx` | شاخه‌ی داشبورد نماینده | | `assets/admin/pages/AppointmentsPage.tsx` | انتخاب endpoint نوبت‌ها بر اساس نقش | | `assets/admin/stores/authStore.ts` | `primaryRole` (از `primary_role` می‌آید — تغییر لازم نیست، فقط مقدار جدید) | | `docs/api/representation.md`, `docs/api/admin.md`, `docs/api/auth.md` | مستندسازی | ## وضعیت فعلی (کد واقعی) `resolvePrimaryRole` — بدون شاخه‌ی نماینده: ```php private function resolvePrimaryRole(User $user): string { $roles = $user->getRoles(); if (in_array('ROLE_ADMIN', $roles, true)) return 'admin'; if (in_array('ROLE_CLINIC', $roles, true)) return 'clinic'; if (in_array('ROLE_DOCTOR', $roles, true)) return 'doctor'; if (in_array('ROLE_SECRETARY', $roles, true)) return 'secretary'; return 'user'; } ``` `AdminApiController` — همه چیز admin-only: ```php #[IsGranted('ROLE_ADMIN')] class AdminApiController extends BaseController { // ... createDoctor(), createClinic(), appointments() همگی ذیل این قانون‌اند } ``` `Doctor` — رابطه با نماینده: ```php #[ORM\Column(name: 'representation_id', type: 'integer', nullable: true)] private ?int $representationId = null; public function getRepresentationId(): ?int { return $this->representationId; } ``` Admin SPA `App.tsx` — RoleRoute و routeهای فعلی: ```tsx function RoleRoute({ roles, children }: { roles: string[]; children: React.ReactNode }) { const primaryRole = useAuthStore(s => s.primaryRole); if (!roles.includes(primaryRole)) return ; return <>{children}; } // ... } /> {/* همه نقش‌ها */} } /> {/* همه نقش‌ها */} } /> // doctors هم اکنون فقط ذیل admin در دسترس است ``` `Sidebar.tsx` — فقط ۴ نقش: ```tsx if (primaryRole === "admin") { ... } if (primaryRole === "clinic") { ... } if (primaryRole === "doctor") { ... } if (primaryRole === "secretary") { ... } // representation وجود ندارد ``` `DashboardPage.tsx` — admin-only endpoints: ```tsx const statsQ = useQuery({ queryFn: () => api.get('/api/v1/admin/dashboard/stats') }); const chartsQ = useQuery({ queryFn: () => api.get(`/api/v1/admin/dashboard/charts?...`) }); const recentQ = useQuery({ queryFn: () => api.get('/api/v1/admin/dashboard/recent') }); ``` `AppointmentsPage.tsx` — انتخاب endpoint فعلی بر اساس نقش: ```ts const isAdmin = primaryRole === 'admin'; const apptEndpoint = isAdmin ? '/api/v1/admin/appointments' : '/api/v1/my/appointments'; ``` ## وظایف ### ۱. Backend — شناختن نقش نماینده در `primary_role` در `resolvePrimaryRole()` شاخه‌ی نماینده را **قبل از `return 'user'`** اضافه کن (اولویت پایین‌تر از admin/clinic/doctor/secretary، چون یک کاربر ممکن است هم‌زمان چند نقش داشته باشد): ```php if (in_array('ROLE_SECRETARY', $roles, true)) return 'secretary'; if (in_array('ROLE_REPRESENTATION', $roles, true)) return 'representation'; return 'user'; ``` اگر در `AuthController` یک gate صریح برای «نقش staff مجاز ورود» هست (لیست ROLE_ADMIN/DOCTOR/CLINIC/SECRETARY که کاربر عادی را رد می‌کند)، `ROLE_REPRESENTATION` را هم به آن لیست اضافه کن تا با `/oauth/token` + `/oauth/userinfo` وارد شود. (اگر چنین gateی وجود ندارد، نیازی نیست.) ### ۲. Backend — اجازه‌ی createDoctor و createClinic به نماینده `AdminApiController` در سطح کلاس `ROLE_ADMIN` است و این قانون بر method-level مقدم می‌شود؛ پس صرفِ گذاشتن قانونِ بازتر روی متد کافی نیست. **کم‌ریسک‌ترین راه: یک controller جدید بساز** — `src/Representation/Controller/RepresentationActionController.php` — با دو route نازک که منطق ساخت پزشک/کلینیک را اجرا کنند (یا منطق مشترک را به یک سرویس استخراج کن و هر دو controller از آن استفاده کنند تا کد تکراری نشود): ```php #[IsGranted(new Expression("is_granted('ROLE_ADMIN') or is_granted('ROLE_REPRESENTATION')"))] #[Route('/api/v1/representation/doctor', methods: ['POST'])] public function createDoctor(Request $request, #[CurrentUser] User $user): JsonResponse { /* همان منطق createDoctor */ } #[IsGranted(new Expression("is_granted('ROLE_ADMIN') or is_granted('ROLE_REPRESENTATION')"))] #[Route('/api/v1/representation/clinic', methods: ['POST'])] public function createClinic(Request $request, #[CurrentUser] User $user): JsonResponse { /* همان منطق createClinic */ } ``` - **مالکیت داده (مهم):** وقتی نماینده پزشک می‌سازد، نماینده‌ی کاربر جاری را با `RepresentationRepository::findByUser($user)` بگیر و `Doctor::setRepresentationId($rep->getId())` را ست کن تا بعداً در فیلتر «نوبت‌های پزشکان من» دیده شود. برای کلینیک هم اگر فیلد مالکیت/شهر مرتبط هست همان را ست کن. - بدنه‌ی request و شکل پاسخ همان قرارداد فعلی `createDoctor`/`createClinic` در `docs/api/admin.md` بماند (تا فرم‌های موجود admin بدون تغییر کار کنند). - در Admin SPA، فرم افزودن پزشک/کلینیک برای نماینده باید این endpointهای جدید را صدا بزند و برای ادمین همان endpointهای `/api/v1/admin/*` را (انتخاب بر اساس `primaryRole`). > اگر تیم ترجیح می‌دهد به‌جای controller جدا، قانون کلاس `AdminApiController` را به Expression تبدیل کند: مراقب باش که در آن صورت **همه‌ی** متدهای آن کلاس باز می‌شوند؛ پس باید روی تک‌تک متدهای admin-only دیگر `#[IsGranted('ROLE_ADMIN')]` گذاشته شود. این پرریسک است؛ controller جدا توصیه می‌شود. ### ۳. Backend — endpoint نوبت‌های پزشکانِ نماینده ``` GET /api/v1/representation/appointments?page=&limit=&status=&from=&to= ``` - `#[IsGranted(new Expression("is_granted('ROLE_REPRESENTATION') or is_granted('ROLE_ADMIN')"))]` - نماینده‌ی کاربر جاری را با `findByUser($user)` پیدا کن؛ اگر نبود → `403`. - در Appointment Repository یک متد `findByRepresentation(int $representationId, array $filters)` اضافه کن که join بزند: `appointment.doctor d` و شرط `d.representationId = :repId`. خروجی با `$this->paginated($items, $total, $page, $limit)`. - شکل هر آیتم همان `Appointment::toArray()` که ادمین مصرف می‌کند، تا `AppointmentsPage` بدون تغییر ساختاری آن را نشان دهد. ### ۴. Backend — endpoint نماینده‌ی کاربر جاری (برای داشبورد) داشبورد فرانت به uuid نماینده نیاز دارد. یک endpoint بساز: ``` GET /api/v1/representation/me ``` - `#[IsGranted('ROLE_REPRESENTATION')]` (یا ادمین‌یا‌نماینده) - نماینده‌ی `#[CurrentUser]` را با `findByUser` برگردان (`$this->success(['data' => $rep->toArray()])`). اگر نبود → `404`. ### ۵. Admin SPA — مسیرها و گارد نقش در `App.tsx`: - مسیرهای `doctors` (لیست/افزودن) و `clinics` (لیست/افزودن) را به `RoleRoute roles={['admin','representation']}` تغییر بده (هر دو زیرمسیر فرم افزودن هم). - `dashboard` و `appointments` (الان «همه نقش‌ها») برای نماینده باز بمانند. - هیچ مسیر admin-only دیگری (`users`, `payments`, `settlements`, `blogs`, `categories`, `sms`, `representations`, ...) را برای نماینده باز **نکن**. ### ۶. Admin SPA — منوی Sidebar برای نماینده در `Sidebar.tsx` شاخه‌ی `if (primaryRole === "representation") { ... }` با آیتم‌ها: - داشبورد (`/admin/dashboard`) - پزشکان (`/admin/doctors`) - کلینیک‌ها (`/admin/clinics`) - نوبت‌ها (`/admin/appointments`) از همان ساختار `Section`/`SectionItem` و آیکن‌های موجود استفاده کن. ### ۷. Admin SPA — داشبورد نماینده در `DashboardPage.tsx` بر اساس `primaryRole` شاخه بزن: - اگر `representation`: اول `GET /api/v1/representation/me` را بگیر تا `uuid` نماینده را داشته باشی، سپس: - `GET /api/v1/representation/{uuid}/dashboard/monthly` - `GET /api/v1/representation/{uuid}/dashboard/yearly` (شکل پاسخ در `docs/api/representation.md`: `total_appointments`، کمیسیون، و...) - کارت‌ها/نمودار را با همین داده‌ها بساز؛ از کارت‌های admin-only (کاربران/پرداخت/تسویه) استفاده نکن. - endpointهای `/api/v1/admin/dashboard/*` نباید برای نماینده صدا زده شوند (۴۰۳ می‌دهند). ### ۸. Admin SPA — صفحه نوبت‌ها برای نماینده در `AppointmentsPage.tsx`: ```ts const isRepresentation = primaryRole === 'representation'; const apptEndpoint = isAdmin ? '/api/v1/admin/appointments' : isRepresentation ? '/api/v1/representation/appointments' : '/api/v1/my/appointments'; ``` - بقیه‌ی فیلترها (status/from/to/page/limit) همان‌طور پاس داده شوند. ### ۹. مستندسازی - `docs/api/auth.md`: مقدار جدید `primary_role: "representation"` و دسترسی این نقش به پنل. - `docs/api/admin.md`: کنار `createDoctor`/`createClinic` ذکر کن که نسخه‌ی نماینده (`/api/v1/representation/doctor` و `/api/v1/representation/clinic`) هم وجود دارد و `representation_id` پزشک خودکار ست می‌شود. - `docs/api/representation.md`: endpointهای جدید `GET /api/v1/representation/appointments`، `GET /api/v1/representation/me`، و `POST /api/v1/representation/doctor|clinic` با method/path/permission/query/response و نمونه JSON واقعی. ## نکات مهم - **اصل امنیت:** نماینده فقط به داده‌ی خودش دسترسی دارد. در همه‌ی endpointهای نماینده، نماینده را از `#[CurrentUser]` و `findByUser` پیدا کن — **هرگز** به `uuid`/`representationId` ورودیِ کلاینت برای تعیین مالکیت اعتماد نکن (الگوی موجود در `RepresentationController`: `$rep->getUser()->getId() !== $user->getId() && !hasRole('ROLE_ADMIN')` → 403). - قانون سطح کلاس `#[IsGranted('ROLE_ADMIN')]` روی `AdminApiController` نباید ضعیف شود؛ ساخت پزشک/کلینیکِ نماینده را در controller جدا بگذار (وظیفه‌ی ۲). - `Doctor.representationId` ستون scalar است (نه association)؛ join/شرط در DQL مستقیم روی همین ستون. - تاریخ‌ها Unix timestamp؛ لیست‌های admin با `paginated()` (items از `data`, total از `meta.totalRecords`). در Admin SPA: paginated → `data?.data` / `data?.meta?.totalRecords`؛ single → `data?.data` (گاهی double-nested). - اگر هیچ Entityی تغییر نکرد migration لازم نیست (این feature عمدتاً منطق/route/permission است). اگر برای مالکیت کلینیکِ نماینده فیلد جدیدی لازم شد، migration بساز و اجرا کن. - بعد از تغییر هر controller، فایل docs مربوطه را همان session به‌روز کن (قانون استاندارد پروژه). - **تست‌ها:** با توکن یک کاربر `ROLE_REPRESENTATION` (`ddev exec php bin/console lexik:jwt:generate-token --user-class="App\\Auth\\Entity\\User"`) چک کن: - `GET /oauth/userinfo` → `primary_role: "representation"` - `POST /api/v1/representation/doctor` و `/clinic` → ۲۰۱ و `representation_id` ست‌شده - `GET /api/v1/representation/appointments` → فقط نوبت‌های پزشکانِ همان نماینده - `GET /api/v1/representation/me` و dashboard ماهانه/سالانه → ۲۰۰ - `GET /api/v1/admin/users` با همان توکن → **۴۰۳** (نباید دسترسی داشته باشد) - در Admin SPA با همان کاربر وارد شو: فقط ۴ آیتم منو دیده شود و افزودن پزشک/کلینیک کار کند.