- Updated authStore to include 'representation' role. - Modified DoctorFormPage and DoctorsPage to handle different endpoints based on user role. - Created new RepresentationActionController for handling doctor and clinic creation by representatives. - Added new API endpoints for representatives to manage doctors, clinics, and view appointments. - Updated documentation to reflect new role and API changes.
16 KiB
دسترسی نقش نماینده (ROLE_REPRESENTATION) به پنل ادمین
پروژه
clinicpro (Backend + Admin React SPA). کاملاً داخل همین پروژه است؛ پرامپت همتای frontend عمومی لازم نیست.
زمینه
کاربرانِ دارای نقش ROLE_REPRESENTATION (نمایندهی فروش/شهر) باید بتوانند وارد پنل ادمین (https://clinic-pro.ddev.site/admin/dashboard) شوند و کارهای محدودِ خودشان را انجام دهند:
- افزودن پزشک
- افزودن کلینیک
- دیدن نوبتهای پزشکانی که زیرمجموعهی همان نمایندهاند (پزشکانی که
Doctor.representationId= id همان نماینده است) - داشبورد اختصاصی خودشان (درآمد/کمیسیون ماهانه و سالانه که از قبل موجود است)
الان این نقش عملاً به پنل راه ندارد و حتی اگر داشته باشد همهچیز مسدود است. شکافهای دقیق:
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 — بدون شاخهی نماینده:
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:
#[IsGranted('ROLE_ADMIN')]
class AdminApiController extends BaseController
{
// ... createDoctor(), createClinic(), appointments() همگی ذیل این قانوناند
}
Doctor — رابطه با نماینده:
#[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های فعلی:
function RoleRoute({ roles, children }: { roles: string[]; children: React.ReactNode }) {
const primaryRole = useAuthStore(s => s.primaryRole);
if (!roles.includes(primaryRole)) return <Navigate to="/admin/dashboard" replace />;
return <>{children}</>;
}
// ...
<Route path="dashboard" element={<DashboardPage />} /> {/* همه نقشها */}
<Route path="appointments" element={<AppointmentsPage />} /> {/* همه نقشها */}
<Route path="clinics" element={<RoleRoute roles={['admin']}><ClinicsPage /></RoleRoute>} />
// doctors هم اکنون فقط ذیل admin در دسترس است
Sidebar.tsx — فقط ۴ نقش:
if (primaryRole === "admin") { ... }
if (primaryRole === "clinic") { ... }
if (primaryRole === "doctor") { ... }
if (primaryRole === "secretary") { ... }
// representation وجود ندارد
DashboardPage.tsx — admin-only endpoints:
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 فعلی بر اساس نقش:
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، چون یک کاربر ممکن است همزمان چند نقش داشته باشد):
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 از آن استفاده کنند تا کد تکراری نشود):
#[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/monthlyGET /api/v1/representation/{uuid}/dashboard/yearly(شکل پاسخ درdocs/api/representation.md:total_appointments، کمیسیون، و...)
- کارتها/نمودار را با همین دادهها بساز؛ از کارتهای admin-only (کاربران/پرداخت/تسویه) استفاده نکن.
- endpointهای
/api/v1/admin/dashboard/*نباید برای نماینده صدا زده شوند (۴۰۳ میدهند).
۸. Admin SPA — صفحه نوبتها برای نماینده
در AppointmentsPage.tsx:
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 <mobile> --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 با همان کاربر وارد شو: فقط ۴ آیتم منو دیده شود و افزودن پزشک/کلینیک کار کند.