feat(resource): update permissions for resource access and enhance booking logic

This commit is contained in:
hamed
2026-08-04 11:05:11 +03:30
parent 429d7ed813
commit 2c7b86d917
5 changed files with 84 additions and 7 deletions
+10 -3
View File
@@ -3,6 +3,13 @@
> **Base:** `/api/v1` · **Auth:** JWT روی همهٔ اندپوینت‌ها
> **مجوز:** `appointment_settings` (`view` خواندن، `update` نوشتن) — همان مجوز تنظیمات
> نوبت‌دهی؛ مجوز تازه‌ای ساخته نشده.
>
> **استثنای خواندن برای نوبت‌دهی (2026-08):** چهار اندپوینتِ خواندنی که ورودیِ ثبت
> نوبت‌اند — `GET /resources`، `GET /resource/{uuid}/services`،
> `GET /resource/{uuid}/day-slots`، `GET /resource/{uuid}/service-slots` — با
> **`appointments.view` هم** باز می‌شوند. منشی یا پزشکِ عضوی که اجازهٔ ثبت نوبت دارد
> ولی تنظیمات نوبت‌دهی برایش بسته است، وگرنه نمی‌توانست همان نوبتی را که مجاز است
> ثبت کند. نوشتن همچنان فقط `appointment_settings.update`.
---
@@ -347,7 +354,7 @@
]
```
**دسترسی:** `appointment_settings.view`. **۴۰۴:** منبع محیط دیگر.
**دسترسی:** `appointment_settings.view` یا `appointments.view`. **۴۰۴:** منبع محیط دیگر.
### `PUT /api/v1/resource/{uuid}/services`
@@ -476,7 +483,7 @@
### `GET /api/v1/resource/{uuid}/day-slots` (2026-08)
مجوز: `appointment_settings.view`. پارامتر: `date=Y-m-d` (الزامی).
مجوز: `appointment_settings.view` یا `appointments.view`. پارامتر: `date=Y-m-d` (الزامی).
بازه‌های **کاری** منبع در یک روز — ورودیِ تایم‌لاینِ سرویسیِ صفحهٔ نوبت‌ها. معادلِ
`appointment-slots` پزشک، ولی از تقویم خودِ منبع: ساعت شعبه ∩ شیفت منبع − تعطیلات −
@@ -499,7 +506,7 @@
### `GET /api/v1/resource/{uuid}/service-slots` (2026-08)
مجوز: `appointment_settings.view`.
مجوز: `appointment_settings.view` یا `appointments.view`.
| پارامتر | توضیح |
|---|---|
@@ -44,7 +44,7 @@ class ResourceBookingSlotController extends BaseController
#[Route('/api/v1/resource/{uuid}/day-slots', name: 'resource_day_slots', methods: ['GET'])]
public function daySlots(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$this->denyUnlessGrantedForBooking($user);
$resource = $this->context->resource($user, $uuid);
$date = $this->requireDate($request);
@@ -77,7 +77,7 @@ class ResourceBookingSlotController extends BaseController
#[Route('/api/v1/resource/{uuid}/service-slots', name: 'resource_service_slots', methods: ['GET'])]
public function serviceSlots(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$this->denyUnlessGrantedForBooking($user);
$resource = $this->context->resource($user, $uuid);
$date = $this->requireDate($request);
@@ -40,7 +40,7 @@ class ResourceController extends BaseController
#[Route('/api/v1/resources', name: 'resource_list', methods: ['GET'])]
public function list(#[CurrentUser] User $user, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$this->denyUnlessGrantedForBooking($user);
[$entityType, $entityId] = $this->context->pair($user);
@@ -155,7 +155,7 @@ class ResourceController extends BaseController
#[Route('/api/v1/resource/{uuid}/services', name: 'resource_services_list', methods: ['GET'])]
public function services(#[CurrentUser] User $user, string $uuid): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$this->denyUnlessGrantedForBooking($user);
return $this->success($this->serviceOfferings->listFor($this->context->resource($user, $uuid)));
}
@@ -5,6 +5,8 @@ namespace App\Resource\Controller;
use App\Auth\Entity\User;
use App\Clinic\Security\ClinicDoctorAccessChecker;
use App\Secretary\Security\SecretaryAccessChecker;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Symfony\Contracts\Service\Attribute\Required;
/**
@@ -34,4 +36,26 @@ trait ResourcePermissionTrait
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', $action);
$this->clinicDoctorAccess->denyUnlessGranted($user, 'appointment_settings', $action);
}
/**
* خواندنِ منبع **برای نوبت‌دهی** — نه برای پیکربندی‌اش.
*
* فهرست منابع، سرویس‌هایشان و وقت‌های آزادشان ورودیِ ثبت نوبت‌اند، پس مجوزِ
* `appointments.view` هم برایشان کافی است: منشی‌ای که اجازهٔ ثبت نوبت دارد ولی
* تنظیمات نوبت‌دهی برایش بسته است، وگرنه نمی‌توانست همان نوبتی را که مجاز است
* ثبت کند — ۴۰۳ روی خواندنِ فهرست، کلِ نوبت‌دهی منبع‌محور را برایش می‌بست.
* نوشتن همچنان فقط با `appointment_settings.update`.
*/
private function denyUnlessGrantedForBooking(User $user): void
{
$allowed =
($this->secretaryAccess->canOrNonSecretary($user, 'appointment_settings', 'view')
&& $this->clinicDoctorAccess->canOrNonMember($user, 'appointment_settings', 'view'))
|| ($this->secretaryAccess->canOrNonSecretary($user, 'appointments', 'view')
&& $this->clinicDoctorAccess->canOrNonMember($user, 'appointments', 'view'));
if (!$allowed) {
throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
}
}
}
@@ -226,6 +226,52 @@ class SecretaryResourceEnforcementTest extends ApiTestCase
$this->assertSame(200, $this->responseCode());
}
/**
* فهرست منابع ورودیِ ثبت نوبت است: منشیِ دارای `appointments.view` — که پیش‌فرض
* است — باید بتواند بخواندش، حتی وقتی `appointment_settings.view` بسته است.
* وگرنه نوبت‌دهی منبع‌محور برایش کاملاً بسته می‌شد.
*/
public function testResourceListReadableWithAppointmentsPermissionOnly(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => [
'appointments' => ['view' => true],
'appointment_settings' => ['view' => false, 'update' => false],
]]);
$this->em->flush();
$this->authJson('GET', '/api/v1/resources', $secretary);
$this->assertSame(200, $this->responseCode());
}
/** نوشتن همچنان فقط با تنظیمات نوبت‌دهی — مجوز نوبت‌ها درش را باز نمی‌کند. */
public function testResourceWriteStillNeedsAppointmentSettings(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => [
'appointments' => ['view' => true, 'create' => true],
'appointment_settings' => ['view' => false, 'update' => false],
]]);
$this->em->flush();
$this->authJson('POST', '/api/v1/resource', $secretary, ['type_uuid' => 'x']);
$this->assertSame(403, $this->responseCode());
}
/** بدون هیچ‌کدام از دو مجوز، فهرست منابع همچنان ۴۰۳ است. */
public function testResourceListDeniedWithoutAnyPermission(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => [
'appointments' => ['view' => false],
'appointment_settings' => ['view' => false],
]]);
$this->em->flush();
$this->authJson('GET', '/api/v1/resources', $secretary);
$this->assertSame(403, $this->responseCode());
}
public function testSubscriptionDeniedByDefault(): void
{
[$secretary] = $this->makeClinicSecretary();