feat(api): add dashboard endpoints for clinic, doctor, and secretary roles
- Implemented GET /api/v1/dashboard/clinic to return clinic stats and today's schedule for clinic owners. - Implemented GET /api/v1/dashboard/doctor to return doctor's stats and today's schedule for doctors. - Implemented GET /api/v1/dashboard/secretary to return stats and conditional appointments for secretaries. feat(migrations): create user_active_context and mobile_verification_otp tables - Added migration to create user_active_context table for tracking active user sessions. - Added migration to create mobile_verification_otp table for handling mobile number verification. feat(migrations): create site_config table for application settings - Added migration to create site_config table to store various site configuration settings. feat(appointments): create MyAppointmentsController for user-specific appointments - Added MyAppointmentsController to handle fetching user-specific appointments with pagination and filtering. feat(auth): implement NotificationMobileController for mobile number verification - Added NotificationMobileController to handle OTP requests and verification for mobile number changes. feat(auth): create MobileVerificationOtp entity for OTP management - Created MobileVerificationOtp entity to manage OTP records for mobile verification. feat(auth): create UserActiveContext entity for user session management - Created UserActiveContext entity to manage user active sessions. feat(config): implement SiteConfigController for managing site settings - Added SiteConfigController to handle fetching and updating site configuration settings. feat(config): create SiteConfig entity and repository for configuration management - Created SiteConfig entity and repository to manage site configuration data.
This commit is contained in:
@@ -3,9 +3,14 @@
|
||||
namespace App\Auth\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Auth\Entity\UserActiveContext;
|
||||
use App\Auth\Repository\UserActiveContextRepository;
|
||||
use App\Auth\Repository\UserRepository;
|
||||
use App\Auth\Service\OtpService;
|
||||
use App\Auth\Service\TokenService;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Secretary\Repository\DoctorSecretaryRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use OpenApi\Attributes as OA;
|
||||
@@ -14,15 +19,20 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[OA\Tag(name: 'Auth')]
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserRepository $userRepo,
|
||||
private readonly OtpService $otpService,
|
||||
private readonly TokenService $tokenService,
|
||||
private readonly RateLimiterFactory $sendCodeLimiter,
|
||||
private readonly UserRepository $userRepo,
|
||||
private readonly OtpService $otpService,
|
||||
private readonly TokenService $tokenService,
|
||||
private readonly RateLimiterFactory $sendCodeLimiter,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly DoctorSecretaryRepository $secretaryRepo,
|
||||
private readonly UserActiveContextRepository $contextRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -459,16 +469,161 @@ class AuthController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_AUTH_001, ErrorCodes::message(ErrorCodes::ERR_AUTH_001), 401);
|
||||
}
|
||||
|
||||
$primaryRole = $this->resolvePrimaryRole($user);
|
||||
$availableContexts = $this->buildAvailableContexts($user);
|
||||
|
||||
// اگر یک context داری، خودکار فعال کن
|
||||
$activeCtx = $this->contextRepo->findByUser($user);
|
||||
if ($activeCtx === null && count($availableContexts) === 1) {
|
||||
$activeCtx = $this->contextRepo->upsert($user, $availableContexts[0]['db_uuid']);
|
||||
}
|
||||
|
||||
$dbUuid = $activeCtx?->getDbUuid();
|
||||
$dbKey = $dbUuid !== null ? $this->buildDbKey($dbUuid) : null;
|
||||
$context = $dbUuid !== null ? $this->findContextByDbUuid($dbUuid, $availableContexts) : null;
|
||||
|
||||
return $this->success([
|
||||
'id' => $user->getId(),
|
||||
'uuid' => $user->getUuid(),
|
||||
'mobile_number' => $user->getMobileNumber(),
|
||||
'realName' => $user->getRealName(),
|
||||
'status' => $user->getStatus(),
|
||||
'roles' => $user->getRoles(),
|
||||
'id' => $user->getId(),
|
||||
'uuid' => $user->getUuid(),
|
||||
'mobile_number' => $user->getMobileNumber(),
|
||||
'realName' => $user->getRealName(),
|
||||
'status' => $user->getStatus(),
|
||||
'roles' => $user->getRoles(),
|
||||
'primary_role' => $primaryRole,
|
||||
'db_uuid' => $dbUuid,
|
||||
'db_key' => $dbKey,
|
||||
'context' => $context,
|
||||
'available_contexts' => $availableContexts,
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/auth/switch-context',
|
||||
summary: 'تغییر محیط کاری فعال',
|
||||
security: [['bearerAuth' => []]],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['db_uuid'],
|
||||
properties: [
|
||||
new OA\Property(property: 'db_uuid', type: 'string', format: 'uuid', description: 'UUID محیط کاری انتخابشده از لیست available_contexts'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Context تغییر کرد'),
|
||||
new OA\Response(response: 401, description: 'توکن وجود ندارد'),
|
||||
new OA\Response(response: 403, description: 'db_uuid در لیست context های این کاربر نیست'),
|
||||
new OA\Response(response: 422, description: 'db_uuid ارسال نشده'),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/auth/switch-context', methods: ['POST'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function switchContext(Request $request, #[CurrentUser] ?User $user): JsonResponse
|
||||
{
|
||||
if ($user === null) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_001, ErrorCodes::message(ErrorCodes::ERR_AUTH_001), 401);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$dbUuid = trim($data['db_uuid'] ?? '');
|
||||
|
||||
if ($dbUuid === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'db_uuid الزامی است', 422);
|
||||
}
|
||||
|
||||
$availableContexts = $this->buildAvailableContexts($user);
|
||||
$matched = $this->findContextByDbUuid($dbUuid, $availableContexts);
|
||||
|
||||
if ($matched === null) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی به این محیط کاری مجاز نیست', 403);
|
||||
}
|
||||
|
||||
$this->contextRepo->upsert($user, $dbUuid);
|
||||
|
||||
return $this->success([
|
||||
'db_uuid' => $dbUuid,
|
||||
'db_key' => $this->buildDbKey($dbUuid),
|
||||
'context' => $matched,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private function resolvePrimaryRole(User $user): string
|
||||
{
|
||||
$roles = $user->getRoles();
|
||||
if (in_array('ROLE_ADMIN', $roles, true)) return 'admin';
|
||||
if (in_array('ROLE_CLINIC', $roles, true)) return 'clinic';
|
||||
if (in_array('ROLE_DOCTOR', $roles, true)) return 'doctor';
|
||||
if (in_array('ROLE_SECRETARY', $roles, true)) return 'secretary';
|
||||
return 'user';
|
||||
}
|
||||
|
||||
private function buildAvailableContexts(User $user): array
|
||||
{
|
||||
$contexts = [];
|
||||
|
||||
// دکتر: مطب شخصی + کلینیکهای عضو
|
||||
if ($doctor = $this->doctorRepo->findByUser($user)) {
|
||||
$contexts[] = [
|
||||
'type' => 'doctor',
|
||||
'db_uuid' => $doctor->getUuid(),
|
||||
'name' => 'مطب شخصی ' . $doctor->getName(),
|
||||
'role' => 'doctor',
|
||||
];
|
||||
foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) {
|
||||
$contexts[] = [
|
||||
'type' => 'clinic',
|
||||
'db_uuid' => $clinic->getUuid(),
|
||||
'name' => $clinic->getName() ?? '',
|
||||
'role' => 'doctor',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// صاحب کلینیک (اگر قبلاً اضافه نشده)
|
||||
if ($clinic = $this->clinicRepo->findByUser($user)) {
|
||||
$alreadyAdded = array_filter($contexts, fn($c) => $c['db_uuid'] === $clinic->getUuid());
|
||||
if (empty($alreadyAdded)) {
|
||||
$contexts[] = [
|
||||
'type' => 'clinic',
|
||||
'db_uuid' => $clinic->getUuid(),
|
||||
'name' => $clinic->getName() ?? '',
|
||||
'role' => 'clinic',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// منشی: همه روابط فعال
|
||||
foreach ($this->secretaryRepo->findAllActiveBySecretary($user) as $rel) {
|
||||
$contexts[] = [
|
||||
'type' => 'doctor',
|
||||
'db_uuid' => $rel->getDoctor()->getUuid(),
|
||||
'name' => 'مطب ' . $rel->getDoctor()->getName(),
|
||||
'role' => 'secretary',
|
||||
'permissions' => $rel->getPermissions(),
|
||||
];
|
||||
}
|
||||
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
private function findContextByDbUuid(string $dbUuid, array $contexts): ?array
|
||||
{
|
||||
foreach ($contexts as $ctx) {
|
||||
if ($ctx['db_uuid'] === $dbUuid) {
|
||||
return $ctx;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function buildDbKey(string $dbUuid): string
|
||||
{
|
||||
return hash_hmac('sha256', $dbUuid, $this->getParameter('kernel.secret'));
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/oauth/logout',
|
||||
summary: 'Logout and optionally revoke refresh token',
|
||||
|
||||
Reference in New Issue
Block a user