feat: Implement secretary permissions enforcement across multiple resources
- Added SecretaryAccessChecker to manage resource access for secretaries. - Integrated permission checks for payments, inventory, and tags in relevant controllers. - Updated PaymentController and PaymentMethodController to enforce secretary permissions. - Enhanced TenantTagController to check permissions for tag management actions. - Introduced tests for secretary resource enforcement, ensuring proper access control. - Updated DoctorSecretary entity to include inventory and tags permissions. - Created a comprehensive audit document for secretary permissions coverage and enforcement. - Fixed potential crashes in SecretaryDashboard when rendering without doctor data.
This commit is contained in:
@@ -18,6 +18,7 @@ use App\Insurance\Repository\TenantInsuranceRepository;
|
||||
use App\Insurance\Repository\TenantServiceCoverageRepository;
|
||||
use App\Insurance\Service\TenantInsuranceService;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Shared\Service\FileValidatorService;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
@@ -44,6 +45,7 @@ class InsuranceController extends BaseController
|
||||
private readonly FileValidatorService $fileValidator,
|
||||
private readonly \App\Clinic\Security\ClinicDoctorPermissionChecker $permChecker,
|
||||
private readonly \App\Patient\Security\PatientRecordScopeResolver $scopeResolver,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
private readonly string $projectDir,
|
||||
) {}
|
||||
|
||||
@@ -252,6 +254,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function getInsurancePricing(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view');
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view');
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
@@ -301,6 +304,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function saveInsurancePricing(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update');
|
||||
@@ -370,6 +374,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function listTenantInsurances(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view');
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view');
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
@@ -400,6 +405,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function activateTenantInsurance(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'create');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update');
|
||||
@@ -435,6 +441,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function updateTenantInsurance(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update');
|
||||
@@ -480,6 +487,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function deactivateTenantInsurance(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'delete');
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'update');
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
@@ -499,6 +507,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function listServiceCoverage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view');
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view');
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
@@ -529,6 +538,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function setServiceCoverage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update');
|
||||
@@ -574,6 +584,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function addDoctorInsurance(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'create');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$doctorId = $data['doctor_id'] ?? null;
|
||||
$insuranceId = $data['insurance_id'] ?? null;
|
||||
@@ -614,6 +625,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function showDoctorInsurance(int $id, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view');
|
||||
$doctorInsurance = $this->doctorInsuranceRepo->find($id);
|
||||
if ($doctorInsurance === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه پزشک یافت نشد', 404);
|
||||
@@ -630,6 +642,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function updateDoctorInsurance(int $id, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update');
|
||||
$doctorInsurance = $this->doctorInsuranceRepo->find($id);
|
||||
if ($doctorInsurance === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه پزشک یافت نشد', 404);
|
||||
@@ -652,6 +665,7 @@ class InsuranceController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function deleteDoctorInsurance(int $id, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'delete');
|
||||
$doctorInsurance = $this->doctorInsuranceRepo->find($id);
|
||||
if ($doctorInsurance === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه پزشک یافت نشد', 404);
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Inventory\Entity\InventoryPackage;
|
||||
use App\Inventory\Repository\InventoryItemRepository;
|
||||
use App\Inventory\Repository\InventoryPackageRepository;
|
||||
use App\Inventory\Service\InventoryService;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Shared\Exception\AppException;
|
||||
@@ -37,6 +38,7 @@ class InventoryController extends BaseController
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly UserActiveContextRepository $contextRepo,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
) {}
|
||||
|
||||
// ── Items ────────────────────────────────────────────────────────────────
|
||||
@@ -44,6 +46,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-items', methods: ['GET'])]
|
||||
public function listItems(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'view');
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
if ($id === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
@@ -60,6 +63,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-categories', methods: ['GET'])]
|
||||
public function listCategories(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'view');
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
if ($id === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
@@ -80,6 +84,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-item', methods: ['POST'])]
|
||||
public function createItem(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'create');
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
if ($id === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
@@ -101,6 +106,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-item/{uuid}', methods: ['PATCH'])]
|
||||
public function updateItem(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'update');
|
||||
$item = $this->ownedItem($uuid, $user);
|
||||
if ($item === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کالا یافت نشد', 404);
|
||||
@@ -123,6 +129,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-item/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteItem(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'delete');
|
||||
$item = $this->ownedItem($uuid, $user);
|
||||
if ($item === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کالا یافت نشد', 404);
|
||||
@@ -138,6 +145,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-packages', methods: ['GET'])]
|
||||
public function listPackages(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'view');
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
if ($id === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
@@ -152,6 +160,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-package', methods: ['POST'])]
|
||||
public function createPackage(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'create');
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
if ($id === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
@@ -173,6 +182,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-package/{uuid}', methods: ['PATCH'])]
|
||||
public function updatePackage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'update');
|
||||
$package = $this->ownedPackage($uuid, $user);
|
||||
if ($package === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پکیج یافت نشد', 404);
|
||||
@@ -199,6 +209,7 @@ class InventoryController extends BaseController
|
||||
#[Route('/api/v1/inventory-package/{uuid}', methods: ['DELETE'])]
|
||||
public function deletePackage(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'delete');
|
||||
$package = $this->ownedPackage($uuid, $user);
|
||||
if ($package === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پکیج یافت نشد', 404);
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Patient\Repository\PatientRecordRepository;
|
||||
use App\Patient\Repository\PatientSessionRepository;
|
||||
use App\Patient\Security\PatientRecordScope;
|
||||
use App\Patient\Security\PatientRecordScopeResolver;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use App\Patient\Service\PatientService;
|
||||
use App\UserProfile\Entity\UserProfile;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
@@ -57,6 +58,7 @@ class PatientController extends BaseController
|
||||
private readonly \App\Discount\Repository\DiscountRuleRepository $discountRuleRepo,
|
||||
private readonly \App\Patient\Repository\SessionPaymentRepository $sessionPaymentRepo,
|
||||
private readonly \App\Patient\Repository\SessionAuditLogRepository $sessionAuditRepo,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
@@ -141,6 +143,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/wallet/charge', methods: ['POST'])]
|
||||
public function chargeWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -176,6 +179,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/wallet/withdraw', methods: ['POST'])]
|
||||
public function withdrawWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -235,6 +239,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/call', methods: ['POST'])]
|
||||
public function createCall(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -271,6 +276,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/call/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteCall(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$call = $this->callRepo->findByUuid($uuid);
|
||||
if ($call === null || !$this->ownsRecord($call->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -302,6 +308,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/message', methods: ['POST'])]
|
||||
public function createMessage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -327,6 +334,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/message/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteMessage(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$message = $this->messageRepo->findByUuid($uuid);
|
||||
if ($message === null || !$this->ownsRecord($message->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -361,6 +369,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/note', methods: ['POST'])]
|
||||
public function createNote(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -383,6 +392,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/note/{uuid}', methods: ['PATCH'])]
|
||||
public function updateNote(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$note = $this->noteRepo->findByUuid($uuid);
|
||||
if ($note === null || !$this->ownsRecord($note->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -408,6 +418,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/note/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteNote(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$note = $this->noteRepo->findByUuid($uuid);
|
||||
if ($note === null || !$this->ownsRecord($note->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -439,6 +450,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/medical-record', methods: ['POST'])]
|
||||
public function createMedicalRecord(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -463,6 +475,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/medical-record/{uuid}', methods: ['PATCH'])]
|
||||
public function updateMedicalRecord(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$medical = $this->medicalRepo->findByUuid($uuid);
|
||||
if ($medical === null || !$this->ownsRecord($medical->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -493,6 +506,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/medical-record/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteMedicalRecord(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$medical = $this->medicalRepo->findByUuid($uuid);
|
||||
if ($medical === null || !$this->ownsRecord($medical->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -524,6 +538,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/attachment', methods: ['POST'])]
|
||||
public function uploadAttachment(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -546,6 +561,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/attachment/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteAttachment(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$attachment = $this->attachmentRepo->findByUuid($uuid);
|
||||
if ($attachment === null || !$this->ownsRecord($attachment->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -683,6 +699,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient', methods: ['POST'])]
|
||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -776,6 +793,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}', methods: ['PATCH'])]
|
||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -985,6 +1003,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/session', methods: ['POST'])]
|
||||
public function createSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1028,6 +1047,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}', methods: ['PATCH'])]
|
||||
public function updateSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1103,6 +1123,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}/payments', methods: ['POST'])]
|
||||
public function addSessionPayment(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1128,6 +1149,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}/payments/{paymentUuid}', methods: ['PATCH'])]
|
||||
public function updateSessionPayment(string $uuid, string $paymentUuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1149,6 +1171,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}/payments/{paymentUuid}', methods: ['DELETE'])]
|
||||
public function deleteSessionPayment(string $uuid, string $paymentUuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'delete');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1186,7 +1209,17 @@ class PatientController extends BaseController
|
||||
|
||||
private function scope(User $user): PatientRecordScope
|
||||
{
|
||||
return $this->scopeCache[$user->getId()] ??= $this->scopeResolver->resolve($user);
|
||||
return $this->scopeCache[$user->getId()] ??= $this->resolveScope($user);
|
||||
}
|
||||
|
||||
/** منشیِ بدون مجوز patients.view هیچ پروندهای نمیبیند؛ سایر نقشها از رزولور. */
|
||||
private function resolveScope(User $user): PatientRecordScope
|
||||
{
|
||||
if ($user->hasRole('ROLE_SECRETARY') && !$this->secretaryAccess->can($user, 'patients', 'view')) {
|
||||
return PatientRecordScope::unknown();
|
||||
}
|
||||
|
||||
return $this->scopeResolver->resolve($user);
|
||||
}
|
||||
|
||||
/** @return array{0: string, 1: int|null} */
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Config\Repository\SiteConfigRepository;
|
||||
use App\Payment\Gateway\GatewayFactory;
|
||||
use App\Payment\Repository\PaymentRepository;
|
||||
use App\Payment\Service\PaymentManager;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use OpenApi\Attributes as OA;
|
||||
@@ -35,6 +36,7 @@ class PaymentController extends BaseController
|
||||
private readonly GatewayFactory $gateways,
|
||||
private readonly PaymentManager $paymentManager,
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
private readonly string $appBaseUrl,
|
||||
private readonly string $allowedFrontendHosts = '',
|
||||
) {}
|
||||
@@ -505,6 +507,7 @@ class PaymentController extends BaseController
|
||||
#[Route('/api/v1/my/payments', methods: ['GET'])]
|
||||
public function myPayments(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'view');
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(1, (int) $request->query->get('limit', 20)));
|
||||
$status = $request->query->get('status');
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\PaymentMethod\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\PaymentMethod\Service\PaymentMethodService;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
@@ -25,15 +26,23 @@ class PaymentMethodController extends BaseController
|
||||
private const ALLOWED_ROLES = ['ROLE_CLINIC', 'ROLE_DOCTOR', 'ROLE_SECRETARY', 'ROLE_ADMIN'];
|
||||
|
||||
public function __construct(
|
||||
private readonly PaymentMethodService $service,
|
||||
private readonly PaymentMethodService $service,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
) {}
|
||||
|
||||
/** نقش مجاز + مجوز منشی روی منبع payments (روشهای پرداخت زیرمجموعهٔ مالی است). */
|
||||
private function guard(User $user, string $action): void
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', $action);
|
||||
}
|
||||
|
||||
// ---- Bank accounts -----------------------------------------------------
|
||||
|
||||
#[Route('/bank-accounts', methods: ['GET'])]
|
||||
public function listBankAccounts(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'view');
|
||||
|
||||
return $this->success($this->service->listBankAccounts($user));
|
||||
}
|
||||
@@ -41,7 +50,7 @@ class PaymentMethodController extends BaseController
|
||||
#[Route('/bank-accounts', methods: ['POST'])]
|
||||
public function createBankAccount(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'create');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->createBankAccount($user, $data), 201);
|
||||
@@ -50,7 +59,7 @@ class PaymentMethodController extends BaseController
|
||||
#[Route('/bank-accounts/{uuid}', methods: ['PUT'])]
|
||||
public function updateBankAccount(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'update');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->updateBankAccount($user, $uuid, $data));
|
||||
@@ -59,7 +68,7 @@ class PaymentMethodController extends BaseController
|
||||
#[Route('/bank-accounts/{uuid}/status', methods: ['PATCH'])]
|
||||
public function toggleBankAccountStatus(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'update');
|
||||
|
||||
return $this->success($this->service->toggleBankAccountStatus($user, $uuid));
|
||||
}
|
||||
@@ -69,7 +78,7 @@ class PaymentMethodController extends BaseController
|
||||
#[Route('/pos', methods: ['GET'])]
|
||||
public function listPos(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'view');
|
||||
|
||||
return $this->success($this->service->listPos($user));
|
||||
}
|
||||
@@ -77,7 +86,7 @@ class PaymentMethodController extends BaseController
|
||||
#[Route('/pos', methods: ['POST'])]
|
||||
public function createPos(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'create');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->createPos($user, $data), 201);
|
||||
@@ -86,7 +95,7 @@ class PaymentMethodController extends BaseController
|
||||
#[Route('/pos/{uuid}', methods: ['PUT'])]
|
||||
public function updatePos(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'update');
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->updatePos($user, $uuid, $data));
|
||||
@@ -95,7 +104,7 @@ class PaymentMethodController extends BaseController
|
||||
#[Route('/pos/{uuid}/status', methods: ['PATCH'])]
|
||||
public function togglePosStatus(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$this->guard($user, 'update');
|
||||
|
||||
return $this->success($this->service->togglePosStatus($user, $uuid));
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ class DoctorSecretary
|
||||
'insurances' => ['view' => true, 'create' => false, 'update' => false, 'delete' => false],
|
||||
'addresses' => ['view' => true, 'create' => false, 'update' => false, 'delete' => false],
|
||||
'clinic_info' => ['view' => true, 'update' => false],
|
||||
'inventory' => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
|
||||
'tags' => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Secretary\Security;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Auth\Repository\UserActiveContextRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Secretary\Entity\DoctorSecretary;
|
||||
use App\Secretary\Repository\DoctorSecretaryRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Exception\AppException;
|
||||
|
||||
/**
|
||||
* Single entry point that answers «آیا این منشی مجاز به resource/action هست؟».
|
||||
*
|
||||
* منبع حقیقت، ستون JSON `permission` روی ردیف فعالِ DoctorSecretary در محیطِ
|
||||
* فعال کاربر (UserActiveContext.db_uuid) است — دقیقاً مثل PatientRecordScopeResolver
|
||||
* و DashboardController::secretary. کنترلرهایی که چند نقش میگیرند فقط وقتی کاربر
|
||||
* ROLE_SECRETARY دارد این checker را صدا میزنند؛ نقشهای دیگر دستنخورده میمانند.
|
||||
*/
|
||||
class SecretaryAccessChecker
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserActiveContextRepository $contextRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly DoctorSecretaryRepository $secretaryRepo,
|
||||
private readonly SecretaryPermissionChecker $permissions,
|
||||
) {}
|
||||
|
||||
/** ردیف فعالِ منشی در محیط فعال؛ null اگر محیط تنظیم نشده یا رابطهای نیست. */
|
||||
public function activeRelation(User $user): ?DoctorSecretary
|
||||
{
|
||||
$dbUuid = $this->contextRepo->findByUser($user)?->getDbUuid();
|
||||
if ($dbUuid === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$clinic = $this->clinicRepo->findByUuid($dbUuid);
|
||||
if ($clinic !== null) {
|
||||
return $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic);
|
||||
}
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid($dbUuid);
|
||||
if ($doctor !== null) {
|
||||
return $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function can(User $user, string $resource, string $action): bool
|
||||
{
|
||||
$relation = $this->activeRelation($user);
|
||||
|
||||
return $relation !== null && $this->permissions->can($relation, $resource, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* برای مسیرهایی که چند نقش دارند: فقط منشی را محدود کن. سایر نقشها true.
|
||||
*/
|
||||
public function canOrNonSecretary(User $user, string $resource, string $action): bool
|
||||
{
|
||||
if (!$user->hasRole('ROLE_SECRETARY')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->can($user, $resource, $action);
|
||||
}
|
||||
|
||||
/** 403 اگر منشی مجاز نباشد؛ نقشهای دیگر بدون تغییر عبور میکنند. */
|
||||
public function denyUnlessGranted(User $user, string $resource, string $action): void
|
||||
{
|
||||
if (!$this->canOrNonSecretary($user, $resource, $action)) {
|
||||
throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ use App\Auth\Entity\User;
|
||||
use App\Auth\Repository\UserActiveContextRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Shared\Exception\AppException;
|
||||
use App\Tag\Entity\TenantTag;
|
||||
use App\Tag\Repository\TenantTagRepository;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
@@ -32,11 +34,22 @@ class TenantTagController extends BaseController
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly UserActiveContextRepository $contextRepo,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
) {}
|
||||
|
||||
/** تگها ابزار پروندهٔ بیمار هم هستند؛ مشاهده با tags.view یا patients.view مجاز است. */
|
||||
private function guardTagView(User $user): void
|
||||
{
|
||||
if (!$this->secretaryAccess->canOrNonSecretary($user, 'tags', 'view')
|
||||
&& !$this->secretaryAccess->canOrNonSecretary($user, 'patients', 'view')) {
|
||||
throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/api/v1/tenant-tags', methods: ['GET'])]
|
||||
public function list(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->guardTagView($user);
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
if ($id === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
@@ -51,6 +64,7 @@ class TenantTagController extends BaseController
|
||||
#[Route('/api/v1/tenant-tag', methods: ['POST'])]
|
||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'tags', 'create');
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
if ($id === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
@@ -79,6 +93,7 @@ class TenantTagController extends BaseController
|
||||
#[Route('/api/v1/tenant-tag/{uuid}', methods: ['PATCH'])]
|
||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'tags', 'update');
|
||||
$tag = $this->ownedTag($uuid, $user);
|
||||
if ($tag === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'برچسب یافت نشد', 404);
|
||||
@@ -111,6 +126,7 @@ class TenantTagController extends BaseController
|
||||
#[Route('/api/v1/tenant-tag/{uuid}', methods: ['DELETE'])]
|
||||
public function delete(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'tags', 'delete');
|
||||
$tag = $this->ownedTag($uuid, $user);
|
||||
if ($tag === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'برچسب یافت نشد', 404);
|
||||
|
||||
Reference in New Issue
Block a user