feat(resource): update permissions for resource access and enhance booking logic
This commit is contained in:
+10
-3
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user