Fix SonarQube: deduplicate scan response, inject appSecret via constructor, reduce returns
ApiLiveController: - Extract buildScanResponse() method (was duplicated 4x for invalid/expired/exit/accepted) - Use reasonMap pattern for state→reason mapping ApiAuthController: - Inject appSecret via constructor (was duplicated '%kernel.secret%' 3x in method params) - Extract tokenResponse() helper (was duplicated " seconds" pattern 3x) - Reduce verifyJwt from 6 returns to 3 using INVALID_JWT constant and merged conditions - tokenResponse(user, includeEmail) handles both login and SSO responses Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,12 +19,16 @@ class ApiAuthController extends AbstractController
|
||||
{
|
||||
private const JWT_TTL = 86400; // 24h
|
||||
|
||||
public function __construct(
|
||||
#[Autowire('%kernel.secret%')] private string $appSecret,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/login', name: 'app_api_auth_login', methods: ['POST'])]
|
||||
public function login(
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
#[Autowire('%kernel.secret%')] string $appSecret,
|
||||
): JsonResponse {
|
||||
$data = json_decode($request->getContent(), true);
|
||||
$email = $data['email'] ?? '';
|
||||
@@ -40,60 +44,40 @@ class ApiAuthController extends AbstractController
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => !$user || !$passwordHasher->isPasswordValid($user, $password) ? 'Identifiants invalides.' : 'Acces reserve aux organisateurs.'], 401);
|
||||
}
|
||||
|
||||
$token = $this->generateJwt($user, $appSecret);
|
||||
$expiresAt = (new \DateTimeImmutable())->modify('+'.self::JWT_TTL.' seconds');
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'expiresAt' => $expiresAt->format(\DateTimeInterface::ATOM),
|
||||
],
|
||||
'error' => null,
|
||||
]);
|
||||
return $this->tokenResponse($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{userId: int|null, expired: bool}
|
||||
*/
|
||||
private const INVALID_JWT = ['userId' => null, 'expired' => false];
|
||||
|
||||
public static function verifyJwt(string $token, string $email, string $appSecret): array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
if (3 !== \count($parts)) {
|
||||
return ['userId' => null, 'expired' => false];
|
||||
return self::INVALID_JWT;
|
||||
}
|
||||
|
||||
[$headerB64, $payloadB64, $signatureB64] = $parts;
|
||||
|
||||
$expectedSignature = self::base64UrlEncode(
|
||||
hash_hmac('sha256', $headerB64.'.'.$payloadB64, $appSecret, true)
|
||||
);
|
||||
$expectedSignature = self::base64UrlEncode(hash_hmac('sha256', $headerB64.'.'.$payloadB64, $appSecret, true));
|
||||
$payload = hash_equals($expectedSignature, $signatureB64) ? json_decode(self::base64UrlDecode($payloadB64), true) : null;
|
||||
|
||||
if (!hash_equals($expectedSignature, $signatureB64)) {
|
||||
return ['userId' => null, 'expired' => false];
|
||||
if (!$payload || ($payload['email'] ?? '') !== $email) {
|
||||
return self::INVALID_JWT;
|
||||
}
|
||||
|
||||
$payload = json_decode(self::base64UrlDecode($payloadB64), true);
|
||||
if (!$payload) {
|
||||
return ['userId' => null, 'expired' => false];
|
||||
}
|
||||
|
||||
if (($payload['email'] ?? '') !== $email) {
|
||||
return ['userId' => null, 'expired' => false];
|
||||
}
|
||||
|
||||
if (($payload['exp'] ?? 0) < time()) {
|
||||
return ['userId' => $payload['userId'] ?? null, 'expired' => true];
|
||||
}
|
||||
|
||||
return ['userId' => $payload['userId'] ?? null, 'expired' => false];
|
||||
return [
|
||||
'userId' => $payload['userId'] ?? null,
|
||||
'expired' => ($payload['exp'] ?? 0) < time(),
|
||||
];
|
||||
}
|
||||
|
||||
#[Route('/refresh', name: 'app_api_auth_refresh', methods: ['POST'])]
|
||||
public function refresh(
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
#[Autowire('%kernel.secret%')] string $appSecret,
|
||||
): JsonResponse {
|
||||
$email = $request->headers->get('ETicket-Email', '');
|
||||
$jwt = $request->headers->get('ETicket-JWT', '');
|
||||
@@ -102,7 +86,7 @@ class ApiAuthController extends AbstractController
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => 'Headers ETicket-Email et ETicket-JWT requis.'], 401);
|
||||
}
|
||||
|
||||
$result = self::verifyJwt($jwt, $email, $appSecret);
|
||||
$result = self::verifyJwt($jwt, $email, $this->appSecret);
|
||||
|
||||
if (null === $result['userId'] || !$result['expired']) {
|
||||
$error = null === $result['userId'] ? 'Token invalide.' : 'Token encore valide, pas besoin de refresh.';
|
||||
@@ -115,17 +99,7 @@ class ApiAuthController extends AbstractController
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => 'Utilisateur introuvable.'], 401);
|
||||
}
|
||||
|
||||
$token = $this->generateJwt($user, $appSecret);
|
||||
$expiresAt = (new \DateTimeImmutable())->modify('+'.self::JWT_TTL.' seconds');
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'expiresAt' => $expiresAt->format(\DateTimeInterface::ATOM),
|
||||
],
|
||||
'error' => null,
|
||||
]);
|
||||
return $this->tokenResponse($user);
|
||||
}
|
||||
|
||||
#[Route('/login/sso', name: 'app_api_auth_sso', methods: ['GET'])]
|
||||
@@ -144,7 +118,6 @@ class ApiAuthController extends AbstractController
|
||||
public function ssoValidate(
|
||||
ClientRegistry $clientRegistry,
|
||||
EntityManagerInterface $em,
|
||||
#[Autowire('%kernel.secret%')] string $appSecret,
|
||||
): JsonResponse {
|
||||
try {
|
||||
$client = $clientRegistry->getClient('keycloak');
|
||||
@@ -171,21 +144,23 @@ class ApiAuthController extends AbstractController
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => 'Acces reserve aux organisateurs.'], 403);
|
||||
}
|
||||
|
||||
$token = $this->generateJwt($user, $appSecret);
|
||||
$expiresAt = (new \DateTimeImmutable())->modify('+'.self::JWT_TTL.' seconds');
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'expiresAt' => $expiresAt->format(\DateTimeInterface::ATOM),
|
||||
'email' => $user->getEmail(),
|
||||
],
|
||||
'error' => null,
|
||||
]);
|
||||
return $this->tokenResponse($user, true);
|
||||
}
|
||||
|
||||
private function generateJwt(User $user, string $appSecret): string
|
||||
private function tokenResponse(User $user, bool $includeEmail = false): JsonResponse
|
||||
{
|
||||
$token = $this->generateJwt($user);
|
||||
$expiresAt = (new \DateTimeImmutable())->modify('+'.self::JWT_TTL.' seconds');
|
||||
|
||||
$data = ['token' => $token, 'expiresAt' => $expiresAt->format(\DateTimeInterface::ATOM)];
|
||||
if ($includeEmail) {
|
||||
$data['email'] = $user->getEmail();
|
||||
}
|
||||
|
||||
return $this->json(['success' => true, 'data' => $data, 'error' => null]);
|
||||
}
|
||||
|
||||
private function generateJwt(User $user): string
|
||||
{
|
||||
$header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
||||
|
||||
@@ -198,7 +173,7 @@ class ApiAuthController extends AbstractController
|
||||
]));
|
||||
|
||||
$signature = self::base64UrlEncode(
|
||||
hash_hmac('sha256', $header.'.'.$payload, $appSecret, true)
|
||||
hash_hmac('sha256', $header.'.'.$payload, $this->appSecret, true)
|
||||
);
|
||||
|
||||
return $header.'.'.$payload.'.'.$signature;
|
||||
|
||||
@@ -210,51 +210,19 @@ class ApiLiveController extends AbstractController
|
||||
return $this->error('Billet introuvable.', 404);
|
||||
}
|
||||
|
||||
if (BilletOrder::STATE_INVALID === $ticket->getState()) {
|
||||
return $this->success([
|
||||
'state' => 'refused',
|
||||
'reason' => 'invalid',
|
||||
'reference' => $ticket->getReference(),
|
||||
'billetName' => $ticket->getBilletName(),
|
||||
'buyerFirstName' => $ticket->getBilletBuyer()->getFirstName(),
|
||||
'buyerLastName' => $ticket->getBilletBuyer()->getLastName(),
|
||||
'isInvitation' => (bool) $ticket->isInvitation(),
|
||||
'firstScannedAt' => $ticket->getFirstScannedAt()?->format(\DateTimeInterface::ATOM),
|
||||
'hasDefinedExit' => $ticket->getBillet()?->hasDefinedExit() ?? false,
|
||||
'details' => null,
|
||||
]);
|
||||
}
|
||||
$reasonMap = [
|
||||
BilletOrder::STATE_INVALID => 'invalid',
|
||||
BilletOrder::STATE_EXPIRED => 'expired',
|
||||
];
|
||||
|
||||
if (BilletOrder::STATE_EXPIRED === $ticket->getState()) {
|
||||
return $this->success([
|
||||
'state' => 'refused',
|
||||
'reason' => 'expired',
|
||||
'reference' => $ticket->getReference(),
|
||||
'billetName' => $ticket->getBilletName(),
|
||||
'buyerFirstName' => $ticket->getBilletBuyer()->getFirstName(),
|
||||
'buyerLastName' => $ticket->getBilletBuyer()->getLastName(),
|
||||
'isInvitation' => (bool) $ticket->isInvitation(),
|
||||
'firstScannedAt' => $ticket->getFirstScannedAt()?->format(\DateTimeInterface::ATOM),
|
||||
'hasDefinedExit' => $ticket->getBillet()?->hasDefinedExit() ?? false,
|
||||
'details' => null,
|
||||
]);
|
||||
if (isset($reasonMap[$ticket->getState()])) {
|
||||
return $this->success($this->buildScanResponse('refused', $reasonMap[$ticket->getState()], $ticket));
|
||||
}
|
||||
|
||||
$hasDefinedExit = $ticket->getBillet()?->hasDefinedExit() ?? false;
|
||||
|
||||
if (null !== $ticket->getFirstScannedAt() && $hasDefinedExit) {
|
||||
return $this->success([
|
||||
'state' => 'refused',
|
||||
'reason' => 'exit_definitive',
|
||||
'reference' => $ticket->getReference(),
|
||||
'billetName' => $ticket->getBilletName(),
|
||||
'buyerFirstName' => $ticket->getBilletBuyer()->getFirstName(),
|
||||
'buyerLastName' => $ticket->getBilletBuyer()->getLastName(),
|
||||
'isInvitation' => (bool) $ticket->isInvitation(),
|
||||
'firstScannedAt' => $ticket->getFirstScannedAt()->format(\DateTimeInterface::ATOM),
|
||||
'hasDefinedExit' => true,
|
||||
'details' => null,
|
||||
]);
|
||||
return $this->success($this->buildScanResponse('refused', 'exit_definitive', $ticket));
|
||||
}
|
||||
|
||||
if (null === $ticket->getFirstScannedAt()) {
|
||||
@@ -262,17 +230,25 @@ class ApiLiveController extends AbstractController
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'state' => 'accepted',
|
||||
'reason' => null,
|
||||
return $this->success($this->buildScanResponse('accepted', null, $ticket));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildScanResponse(string $state, ?string $reason, BilletOrder $ticket): array
|
||||
{
|
||||
return [
|
||||
'state' => $state,
|
||||
'reason' => $reason,
|
||||
'reference' => $ticket->getReference(),
|
||||
'billetName' => $ticket->getBilletName(),
|
||||
'buyerFirstName' => $ticket->getBilletBuyer()->getFirstName(),
|
||||
'buyerLastName' => $ticket->getBilletBuyer()->getLastName(),
|
||||
'isInvitation' => (bool) $ticket->isInvitation(),
|
||||
'firstScannedAt' => $ticket->getFirstScannedAt()?->format(\DateTimeInterface::ATOM),
|
||||
'hasDefinedExit' => $hasDefinedExit,
|
||||
'details' => [],
|
||||
]);
|
||||
'hasDefinedExit' => $ticket->getBillet()?->hasDefinedExit() ?? false,
|
||||
'details' => 'accepted' === $state ? [] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user