feat(secretary): grant staff/discounts/sms/appointment_settings/clinic_doctors (phase B)
Extends the secretary permission system to five previously owner-only modules, so a clinic/doctor can delegate each page to a secretary. All were unreachable by secretaries before (role-based tenant resolution returned "unknown" → 403). New permission resources (default-deny, three-place add: entity default, SecretaryPermissions type, both MySecretariesPage + admin SecretariesPage): staff, discounts, sms, appointment_settings (view/update only), clinic_doctors (clinic-only — hidden from independent doctors via `clinicOnly` section filter). Backend enforcement (SecretaryAccessChecker, three new reusable helpers): - resolveOwnerEntity(): owner pair from active context — used by StaffController, DiscountController, SmsWalletController (now secretary-aware resolveEntity). - canForDoctor(): per-doctor-scoped check (assigned doctor + toggle) — wired into AppointmentSettingsController::denyDoctorAccess. - canForClinic(): clinic-scoped check — wired into ClinicController::detachDoctor, ClinicDoctorPermissionController (view/update), ClinicInvitationController (create/view/update/delete). clinic_doctors is clinic-context only. Guards run ahead of any subscription gate; non-secretary roles pass unchanged. Frontend: - RoleRoute: staff, discounts, sms-wallet, appointment-settings (doctor+clinic variants), settings/clinic-doctors routes accept secretary + permission gate. - Sidebar (secretary branch): five new items gated by can(); appointment_settings route follows active scope; clinic_doctors only in clinic scope. Tests: SecretaryResourceEnforcementTest — denied-by-default + allowed-when-granted for all five (18 total). Sidebar.test — B-resource gating + clinic_doctors scope rule. docs/api/secretary.md resource list, enforcement map, JSON example updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-2
@@ -87,7 +87,12 @@ Create a secretary for a doctor.
|
||||
"create": false,
|
||||
"update": false,
|
||||
"delete": false
|
||||
}
|
||||
},
|
||||
"staff": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"discounts": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"sms": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"appointment_settings": { "view": false, "update": false },
|
||||
"clinic_doctors": { "view": false, "create": false, "update": false, "delete": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +130,7 @@ Create a secretary for a doctor.
|
||||
|
||||
**Permissions Structure:**
|
||||
|
||||
مجموعهٔ منابع (resources) بر اساس صفحات و ماژولهای در دسترسِ منشی است: `appointments`, `patients`, `payments`, `insurances`, `addresses`, `clinic_info`, `inventory`, `tags`, `services`. `mergePermissions` هر منبع/اکشن ارسالشده را deep-merge میکند. منابع `inventory`, `tags`, `services` بهصورت پیشفرض همه `false`اند (default-deny)؛ بقیه طبق `DEFAULT_PERMISSIONS`.
|
||||
مجموعهٔ منابع (resources) بر اساس صفحات و ماژولهای در دسترسِ منشی است: `appointments`, `patients`, `payments`, `insurances`, `addresses`, `clinic_info`, `inventory`, `tags`, `services`, `staff`, `discounts`, `sms`, `appointment_settings`, `clinic_doctors`. `mergePermissions` هر منبع/اکشن ارسالشده را deep-merge میکند. منابعِ `inventory`, `tags`, `services`, `staff`, `discounts`, `sms`, `appointment_settings`, `clinic_doctors` بهصورت پیشفرض همه `false`اند (default-deny)؛ بقیه طبق `DEFAULT_PERMISSIONS`. منبعِ `appointment_settings` فقط `view/update` دارد. منبعِ `clinic_doctors` **فقط در حالت کلینیک** معنا دارد (پزشک مستقل نه toggle نه منو).
|
||||
|
||||
**اعمال (enforcement):** همهٔ منابع در بکاند enforce میشوند، نه فقط `appointments`. منبعِ حقیقت، ستون JSON `permission` روی ردیفِ فعالِ `DoctorSecretary` در محیطِ فعالِ کاربر (`UserActiveContext.db_uuid`) است؛ نقطهٔ مرکزی `App\Secretary\Security\SecretaryAccessChecker` (`can` / `canOrNonSecretary` / `denyUnlessGranted`). نبودِ مجوز → `403 ERR_FORBIDDEN_001`. نقشه:
|
||||
|
||||
@@ -138,6 +143,11 @@ Create a secretary for a doctor.
|
||||
| `inventory` | `InventoryController` (items + packages) | view/create/update/delete |
|
||||
| `tags` | `TenantTagController` (لیست با `tags.view` یا `patients.view`؛ نوشتنها با `tags.*`) | view/create/update/delete |
|
||||
| `services` | `ClinicServiceController` (sections + items + tariffs). owner از محیطِ فعال با `SecretaryAccessChecker::resolveOwnerEntity` حل میشود چون `EntityContextResolver` منشی را نمیشناسد. گیتِ `services.*` پیش از گیتِ اشتراک اجرا میشود | view/create/update/delete |
|
||||
| `staff` | `StaffController` (resolveEntity منشیآگاه) | view/create/update/delete |
|
||||
| `discounts` | `DiscountController` (CRUD؛ `suggestions` جزو flowِ جلسه است و با discounts گِیت نمیشود) | view/create/update/delete |
|
||||
| `sms` | `SmsWalletController` (balance/charge/logs/settings). endpointهای admin (قالب/ارسال) همچنان `ROLE_ADMIN` | view/create/update |
|
||||
| `appointment_settings` | `AppointmentSettingsController::denyDoctorAccess` → `SecretaryAccessChecker::canForDoctor` (اسکوپِ پزشکِ تخصیصیافته + توگل). `clinic_uuid` برای محیطِ کلینیک لازم است | view/update |
|
||||
| `clinic_doctors` (فقط کلینیک) | `ClinicController::detachDoctor` (delete)، `ClinicDoctorPermissionController` (view/update)، `ClinicInvitationController` (create/view/update/delete) via `SecretaryAccessChecker::canForClinic` | view/create/update/delete |
|
||||
|
||||
نقشهای غیرمنشی (`ROLE_CLINIC`/`ROLE_DOCTOR`/`ROLE_ADMIN`) از این چک عبور میکنند (`canOrNonSecretary` برایشان `true`). منشیِ بدون رابطهٔ فعال/context هیچ مجوزی ندارد → همهچیز `403`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user