feat(secretary): add services permission resource + panel gating (phase A)
Secretaries could reach neither the services module (EntityContextResolver does not recognise a secretary as clinic owner, so they resolved to `unknown` → 403) nor had any toggle to grant it. Add `services` as a first-class secretary permission resource, enforced end-to-end. Backend - DoctorSecretary::DEFAULT_PERMISSIONS: new `services` resource (default-deny). - SecretaryAccessChecker::resolveOwnerEntity(): reusable owner (clinic/doctor) resolution from the secretary's active context, for controllers whose data is fetched by [entityType, entityId] and whose generic resolver is not secretary-aware. - ClinicServiceController: resolveEntity() is now secretary-aware; every action (sections, items, tariffs — 13 total) guards with `services` view/create/ update/delete via denyUnlessGranted, ahead of the subscription gate. Frontend - SecretaryPermissions type + MySecretariesPage + SecretariesPage: `services` section so owners can grant it. - Sidebar (secretary branch): services / inventory / tags menu items gated by can(resource, 'view'). - RoleRoute: a secretary now needs the page's `permission` to open it (direct URL entry included); clinic-services, inventory, tags-settings routes accept secretary + permission gate. Tests - SecretaryResourceEnforcementTest: services denied-by-default, allowed-when- granted, create-denied-while-view-granted. - Sidebar.test: secretary menu gating for services/inventory/tags. Docs: secretary.md + clinic-services.md updated with the `services` resource and the resolveOwnerEntity note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-5
@@ -121,7 +121,11 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
|
||||
function RoleRoute({ roles, blockClinicScope, permission, children }: {
|
||||
roles: string[];
|
||||
blockClinicScope?: boolean;
|
||||
/** [resource, action] — پزشکِ مهمان با داشتن این مجوز از blockClinicScope مستثنا میشود. */
|
||||
/**
|
||||
* [resource, action] — گیت مجوز صفحه.
|
||||
* • پزشکِ مهمان در محیط کلینیک با داشتن این مجوز از blockClinicScope مستثنا میشود.
|
||||
* • منشی فقط با داشتن این مجوز به صفحه دسترسی دارد (همراستا با enforcement سمت API).
|
||||
*/
|
||||
permission?: [string, string];
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
@@ -130,6 +134,10 @@ function RoleRoute({ roles, blockClinicScope, permission, children }: {
|
||||
const { can } = usePermissions();
|
||||
if (!primaryRole) return <div style={{ padding: 40, textAlign: 'center' }}>در حال بارگذاری...</div>;
|
||||
if (!roles.includes(primaryRole)) return <Navigate to="/admin/dashboard" replace />;
|
||||
// منشیِ بدون مجوزِ این صفحه نباید با ورود مستقیم URL هم بازش کند.
|
||||
if (primaryRole === 'secretary' && permission && !can(permission[0], permission[1])) {
|
||||
return <Navigate to="/admin/dashboard" replace />;
|
||||
}
|
||||
// پزشکِ مهمان در محیط کلینیک فقط تا جایی که کلینیک مجوز داده دسترسی دارد.
|
||||
if (blockClinicScope && primaryRole === 'doctor' && context?.scope === 'clinic') {
|
||||
if (!permission || !can(permission[0], permission[1])) {
|
||||
@@ -243,14 +251,14 @@ export default function App() {
|
||||
<Route path="staff" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><StaffPage /></RoleRoute>} />
|
||||
<Route path="settings-menu" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><SettingsMenuPage /></RoleRoute>} />
|
||||
<Route path="account-settings" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']}><AccountSettingsPage /></RoleRoute>} />
|
||||
<Route path="tags-settings" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><TagsSettingsPage /></RoleRoute>} />
|
||||
<Route path="tags-settings" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['tags', 'view']}><TagsSettingsPage /></RoleRoute>} />
|
||||
<Route path="appointment-settings" element={<RoleRoute roles={['doctor']} blockClinicScope><AppointmentSettingsPage /></RoleRoute>} />
|
||||
<Route path="subscription" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><SubscriptionPage /></RoleRoute>} />
|
||||
<Route path="discounts" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><DiscountsPage /></RoleRoute>} />
|
||||
<Route path="subscription/success" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><PaymentSuccessPage /></RoleRoute>} />
|
||||
<Route path="clinic-services" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><ClinicServicesPage /></RoleRoute>} />
|
||||
<Route path="clinic-services/:uuid" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><ServiceDetailPage /></RoleRoute>} />
|
||||
<Route path="inventory" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><InventoryPage /></RoleRoute>} />
|
||||
<Route path="clinic-services" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['services', 'view']}><ClinicServicesPage /></RoleRoute>} />
|
||||
<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="sms-wallet" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><SmsWalletPage /></RoleRoute>} />
|
||||
<Route path="my-secretaries" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><MySecretariesPage /></RoleRoute>} />
|
||||
<Route path="admin-subscription" element={<RoleRoute roles={['admin']}><AdminSubscriptionPage /></RoleRoute>} />
|
||||
|
||||
Reference in New Issue
Block a user