feat: add per-doctor permissions management in clinics

- Implement DoctorPermissionsModal for managing doctor permissions in clinics.
- Create usePermissions hook to handle user permissions context.
- Add migration for clinic_doctor_permissions table with default permissions.
- Develop ClinicDoctorPermissionController for handling permissions API.
- Create ClinicDoctorPermission entity to manage permissions data.
- Implement ClinicDoctorPermissionRepository for database interactions.
- Add ClinicDoctorPermissionChecker for permission validation logic.
- Write tests for clinic doctor permissions functionality.
This commit is contained in:
hamed
2026-07-18 09:44:13 +03:30
parent 3a23aa242e
commit e4ddd38f0c
17 changed files with 1504 additions and 44 deletions
+29 -3
View File
@@ -8,6 +8,8 @@ use App\Auth\Repository\UserActiveContextRepository;
use App\Auth\Repository\UserRepository;
use App\Auth\Service\OtpService;
use App\Auth\Service\TokenService;
use App\Clinic\Entity\ClinicDoctorPermission;
use App\Clinic\Repository\ClinicDoctorPermissionRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Secretary\Repository\DoctorSecretaryRepository;
@@ -37,6 +39,7 @@ class AuthController extends BaseController
private readonly RateLimiterFactory $passwordResetLimiter,
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
private readonly ClinicDoctorPermissionRepository $clinicDoctorPermRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly UserActiveContextRepository $contextRepo,
private readonly UserPasswordHasherInterface $hasher,
@@ -703,11 +706,18 @@ class AuthController extends BaseController
'name' => 'مطب شخصی ' . $doctor->getName(),
'role' => 'doctor',
];
foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) {
// پزشکِ عضو کلینیک «مالک» نیست؛ نقش doctor با scope کلینیک می‌گیرد تا
// فقط نوبت‌های خودش در آن کلینیک را ببیند، نه دسترسی کامل پنل کلینیک.
$memberClinics = $this->clinicRepo->findByDoctor($doctor);
$permMap = $this->clinicDoctorPermRepo->mapByClinicForDoctor(
$doctor,
array_map(fn($c) => $c->getId(), $memberClinics),
);
foreach ($memberClinics as $clinic) {
// پزشکِ عضو کلینیک «مالک» نیست؛ نقش doctor با scope کلینیک می‌گیرد و
// دسترسی‌اش را مجوزهای همان کلینیک تعیین می‌کند، نه hardcode.
// اگر همین پزشک مالک کلینیک باشد، نقش کامل clinic در بلوک مالک پایین ست می‌شود.
$isOwner = $clinic->getUser()->getId() === $user->getId();
$perm = $permMap[$clinic->getId()] ?? null;
$contexts[] = [
'type' => 'clinic',
'db_uuid' => $clinic->getUuid(),
@@ -715,6 +725,7 @@ class AuthController extends BaseController
'role' => $isOwner ? 'clinic' : 'doctor',
'scope' => $isOwner ? null : 'clinic',
'doctor_uuid' => $doctor->getUuid(),
'permissions' => $isOwner ? null : $this->contextPermissions($perm),
];
}
}
@@ -764,6 +775,21 @@ class AuthController extends BaseController
return $contexts;
}
/**
* مجوزی که به کلاینت داده می‌شود: نبودِ سطر یعنی عضویت قدیمی (پیش‌فرض)، و
* سطر غیرفعال یعنی هیچ دسترسی.
*/
private function contextPermissions(?ClinicDoctorPermission $perm): array
{
if ($perm === null) {
return ClinicDoctorPermission::DEFAULT_PERMISSIONS;
}
return $perm->isActive()
? $perm->getPermissions()
: ['version' => 1, 'resources' => []];
}
private function findContextByDbUuid(string $dbUuid, array $contexts): ?array
{
foreach ($contexts as $ctx) {