feat: add ClinicInvitationWebController and related templates for handling clinic invitations
- Implemented ClinicInvitationWebController to manage the invitation process via web. - Added view and respond methods to handle invitation display and responses. - Created result.html.twig and view.html.twig templates for rendering invitation results and views. - Integrated CSRF protection for form submissions. - Established routes for invitation viewing and responding.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\ClinicInvitation\Controller;
|
||||
|
||||
use App\ClinicInvitation\Entity\ClinicDoctorInvitation;
|
||||
use App\ClinicInvitation\Repository\ClinicDoctorInvitationRepository;
|
||||
use App\ClinicInvitation\Service\ClinicInvitationService;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Shared\Exception\AppException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* صفحات HTML عمومی دعوت پزشک — لینک پیامک اینجا باز میشود (بدون JWT).
|
||||
* جدا از ClinicInvitationController که نسخهٔ JSON (/api/v1/...) را برای کلاینت React/اپ سِرو میکند.
|
||||
*/
|
||||
class ClinicInvitationWebController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClinicDoctorInvitationRepository $invRepo,
|
||||
private readonly ClinicInvitationService $invitationService,
|
||||
private readonly string $appUrl,
|
||||
) {}
|
||||
|
||||
#[Route('/clinic-invitation/{token}', methods: ['GET'], name: 'invitation_web_view')]
|
||||
#[Route('/i/{token}', methods: ['GET'], name: 'invitation_web_view_short')]
|
||||
public function view(string $token): Response
|
||||
{
|
||||
$inv = $this->invRepo->findByToken($token);
|
||||
if ($inv === null) {
|
||||
return $this->render('invitation/result.html.twig', ['state' => 'notfound'])->setStatusCode(404);
|
||||
}
|
||||
|
||||
if (!$inv->isUsable()) {
|
||||
return $this->render('invitation/result.html.twig', [
|
||||
'state' => $this->stateFromStatus($inv),
|
||||
'admin_url' => $this->adminUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('invitation/view.html.twig', [
|
||||
'token' => $token,
|
||||
'clinic' => $inv->getClinic()->getName() ?: 'کلینیک',
|
||||
'doctor' => $inv->getInvitedName(),
|
||||
'expires_at' => $inv->getExpiresAt(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/clinic-invitation/{token}/respond', methods: ['POST'], name: 'invitation_web_respond')]
|
||||
public function respond(string $token, Request $request): Response
|
||||
{
|
||||
$inv = $this->invRepo->findByToken($token);
|
||||
if ($inv === null) {
|
||||
return $this->render('invitation/result.html.twig', ['state' => 'notfound'])->setStatusCode(404);
|
||||
}
|
||||
|
||||
if (!$this->isCsrfTokenValid('invitation_' . $token, (string) $request->request->get('_token'))) {
|
||||
return $this->render('invitation/result.html.twig', ['state' => 'expired'])->setStatusCode(403);
|
||||
}
|
||||
|
||||
$action = (string) $request->request->get('action', '');
|
||||
|
||||
try {
|
||||
if ($action === 'accept') {
|
||||
$this->invitationService->accept($inv);
|
||||
return $this->render('invitation/result.html.twig', [
|
||||
'state' => 'accepted',
|
||||
'admin_url' => $this->adminUrl(),
|
||||
]);
|
||||
}
|
||||
if ($action === 'reject') {
|
||||
$this->invitationService->reject($inv);
|
||||
return $this->render('invitation/result.html.twig', ['state' => 'rejected']);
|
||||
}
|
||||
} catch (AppException) {
|
||||
return $this->render('invitation/result.html.twig', ['state' => 'expired']);
|
||||
}
|
||||
|
||||
return $this->render('invitation/result.html.twig', ['state' => 'expired'])->setStatusCode(422);
|
||||
}
|
||||
|
||||
private function stateFromStatus(ClinicDoctorInvitation $inv): string
|
||||
{
|
||||
return match ($inv->getStatus()) {
|
||||
ClinicDoctorInvitation::STATUS_ACCEPTED => 'accepted',
|
||||
ClinicDoctorInvitation::STATUS_REJECTED => 'rejected',
|
||||
default => 'expired',
|
||||
};
|
||||
}
|
||||
|
||||
private function adminUrl(): string
|
||||
{
|
||||
return rtrim($this->appUrl, '/') . '/admin';
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ class ClinicDoctorInvitation
|
||||
$this->clinic = $clinic;
|
||||
$this->invitedBy = $invitedBy;
|
||||
$this->mobile = $mobile;
|
||||
$this->token = bin2hex(random_bytes(16));
|
||||
$this->token = bin2hex(random_bytes(6));
|
||||
$this->invitedAt = time();
|
||||
$this->expiresAt = $this->invitedAt + 72 * 3600;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ class ClinicDoctorInvitation
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->token = bin2hex(random_bytes(16));
|
||||
$this->token = bin2hex(random_bytes(6));
|
||||
$this->tokenUsed = false;
|
||||
$this->invitedAt = time();
|
||||
$this->expiresAt = $this->invitedAt + 72 * 3600;
|
||||
|
||||
@@ -110,7 +110,7 @@ class ClinicInvitationService
|
||||
private function sendSms(ClinicDoctorInvitation $inv, Clinic $clinic): void
|
||||
{
|
||||
$clinicName = $clinic->getName() ?? 'کلینیک';
|
||||
$link = rtrim($this->appUrl, '/') . '/clinic-invitation/' . $inv->getToken();
|
||||
$link = rtrim($this->appUrl, '/') . '/i/' . $inv->getToken();
|
||||
|
||||
$this->smsService->dispatchTemplate(\App\Sms\Entity\SmsLog::TAG_CLINIC_INVITATION, $inv->getMobile(), [
|
||||
'clinic' => $clinicName,
|
||||
|
||||
Reference in New Issue
Block a user