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:
hamed
2026-06-11 12:20:12 +03:30
parent 54c491c734
commit e7b90a6399
32 changed files with 3780 additions and 354 deletions
+165 -10
View File
@@ -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',