feat: implement OTP login flow and enhance role-based access control

- Added OTP login functionality in driver.mjs to handle user authentication with a fixed code in dev environment.
- Enhanced RoleRoute component in App.tsx to support clinic-scoped doctor roles and permissions.
- Updated ClinicDoctorsManager component to include pagination and search functionality for better user experience.
- Refactored tests for ClinicDoctorsManager to cover new features and ensure proper API mocking.
- Adjusted permissions in settingsMenu.ts and PracticeDomainSettingsPage.tsx to align with updated backend requirements.
- Created RoleRoute.test.tsx to validate role-based access logic for different user scenarios.
This commit is contained in:
hamed
2026-08-09 12:41:11 +03:30
parent cfeb447645
commit 60ccd5cc1d
8 changed files with 515 additions and 121 deletions
+22 -3
View File
@@ -144,7 +144,7 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
return isAuthenticated ? <Navigate to="/admin/dashboard" replace /> : <>{children}</>;
}
function RoleRoute({ roles, blockClinicScope, permission, children }: {
export function RoleRoute({ roles, blockClinicScope, permission, children }: {
roles: string[];
blockClinicScope?: boolean;
/**
@@ -159,7 +159,23 @@ function RoleRoute({ roles, blockClinicScope, permission, children }: {
const context = useAuthStore((s) => s.context);
const { can } = usePermissions();
if (!primaryRole) return <div style={{ padding: 40, textAlign: 'center' }}>در حال بارگذاری...</div>;
if (!roles.includes(primaryRole)) return <Navigate to="/admin/dashboard" replace />;
/**
* پزشکِ عضو در محیط کلینیک برای گِیتِ نقشی همان «clinic» حساب می‌شود — ولی **فقط**
* وقتی صفحه مجوزی اعلام کرده و کلینیک آن مجوز را به او داده باشد.
*
* بدون این، منو و route با هم نمی‌خواندند: `settingsMenu` واریانتِ نقشیِ همین پزشک را
* با `clinic` تطبیق می‌داد و آیتم را نشان می‌داد، ولی اینجا `primaryRole` او `doctor`
* بود و کلیک به داشبورد می‌پرید — یعنی منویی که کار نمی‌کرد.
*
* قید «مجوز الزامی» عمدی است: بدون آن، هر صفحهٔ مخصوصِ مالک برای هر پزشکِ عضو باز
* می‌شد. با آن، دسترسی همان چیزی می‌ماند که مالک کلینیک صراحتاً داده است.
*/
const clinicScopedDoctor = primaryRole === 'doctor' && context?.scope === 'clinic';
const roleOk = roles.includes(primaryRole)
|| (clinicScopedDoctor && roles.includes('clinic') && !!permission && can(permission[0], permission[1]));
if (!roleOk) return <Navigate to="/admin/dashboard" replace />;
// منشیِ بدون مجوزِ این صفحه نباید با ورود مستقیم URL هم بازش کند.
if (primaryRole === 'secretary' && permission && !can(permission[0], permission[1])) {
return <Navigate to="/admin/dashboard" replace />;
@@ -297,7 +313,10 @@ export default function App() {
<Route path="clinic-services/:uuid" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['services', 'view']}><ServiceDetailPage /></RoleRoute>} />
<Route path="inventory" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['inventory', 'view']}><InventoryPage /></RoleRoute>} />
<Route path="treatment-cases" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} permission={['treatment', 'view']}><TreatmentCasesPage /></RoleRoute>} />
<Route path="settings/practice-domain" element={<RoleRoute roles={['clinic']} permission={['appointment_settings', 'view']}><PracticeDomainSettingsPage /></RoleRoute>} />
{/* مجوزش `clinic_info` است نه `appointment_settings`: صفحه حوزهٔ فعالیت را با
PATCH /api/v1/clinic/{uuid} ذخیره می‌کند و backend همان‌جا `clinic_info.update`
را می‌سنجد. گِیتِ قبلی منبع دیگری را می‌پرسید و با اجبارِ واقعی نمی‌خواند. */}
<Route path="settings/practice-domain" element={<RoleRoute roles={['clinic']} permission={['clinic_info', 'view']}><PracticeDomainSettingsPage /></RoleRoute>} />
<Route path="practice-domains" element={<RoleRoute roles={['admin']}><AdminPracticeDomainsPage /></RoleRoute>} />
<Route path="service-categories" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['services', 'view']}><CatalogCategoriesPage /></RoleRoute>} />
<Route path="resources" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['resources', 'view']}><ResourcesPage /></RoleRoute>} />