feat: Implement permission gate for appointment and billing controllers

- Added PermissionGateTrait to manage access control for AppointmentPlanController and BillingController.
- Introduced denyUnlessGrantedForPlanning method in AppointmentPlanController to handle specific permission checks for planning appointments.
- Updated existing methods in both controllers to utilize the new permission checks.
- Refactored ResourcePermissionTrait to use PermissionGateTrait for cleaner permission management.
- Added tests to ensure proper permission enforcement across different scenarios, including cross-tenant access restrictions for staff.
This commit is contained in:
hamed
2026-08-08 10:27:13 +03:30
parent c452150a83
commit 934405c42d
14 changed files with 830 additions and 67 deletions
@@ -29,6 +29,17 @@ use OpenApi\Attributes as OA;
#[OA\Tag(name: 'My Appointments')]
class MyAppointmentsController extends BaseController
{
use \App\Shared\Controller\PermissionGateTrait;
/**
* همهٔ روت‌های این کنترلر زیرِ توگلِ «مدیریت نوبت‌ها»ی پنل‌اند: فهرست، آمار روز،
* تب‌های پزشک و خودِ فرمِ ثبت نوبت. `patient-lookup` هم بخشی از همان فرم است.
*/
private function permissionResource(): string
{
return 'appointments';
}
public function __construct(
private readonly EntityManagerInterface $em,
private readonly AppointmentRepository $appointmentRepo,
@@ -63,6 +74,8 @@ class MyAppointmentsController extends BaseController
#[IsGranted('IS_AUTHENTICATED_FULLY')]
public function myClinicDoctors(#[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$roles = $user->getRoles();
$doctors = [];
@@ -116,6 +129,8 @@ class MyAppointmentsController extends BaseController
return $this->error(ErrorCodes::FORBIDDEN, 'دسترسی ندارید', 403);
}
$this->denyUnlessGranted($user, 'create');
$data = json_decode($request->getContent(), true) ?? [];
$doctorUuid = trim($data['doctor_uuid'] ?? '');
$slotStart = (int) ($data['slot_start'] ?? 0);
@@ -394,6 +409,10 @@ class MyAppointmentsController extends BaseController
return $this->error(ErrorCodes::FORBIDDEN, 'دسترسی ندارید', 403);
}
// بخشی از فرمِ ثبت نوبت است، پس همان `appointments.create` — نه `patients.view`.
// با گیتِ پرونده، منشی‌ای که فقط اجازهٔ نوبت‌دهی دارد فرمش را از دست می‌داد.
$this->denyUnlessGranted($user, 'create');
// Lookup by mobile OR national code — the booking form lets the user
// search either way. National code takes precedence when both are sent.
$mobile = InputValidator::toEnglishDigits(trim((string) $request->query->get('mobile', '')));
@@ -430,6 +449,8 @@ class MyAppointmentsController extends BaseController
#[IsGranted('IS_AUTHENTICATED_FULLY')]
public function myAppointments(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(500, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
@@ -620,6 +641,8 @@ class MyAppointmentsController extends BaseController
#[IsGranted('IS_AUTHENTICATED_FULLY')]
public function todayStats(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$date = trim((string) $request->query->get('date', date('Y-m-d')));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$date = date('Y-m-d');