refactor: address static analysis warnings and reduce code duplication

- Created UserManagementService to centralize common user creation logic.
- Refactored ClientsController and RevendeursController to use UserManagementService.
- Reduced code duplication in StatsController and RgpdService.
- Fixed type mismatch in StatusController for ServiceMessage author.
- Improved data consistency in MailerService and EmailTrackingController for attachments.
- Added missing MessengerLogRepository.
- Fixed getFlashBag() call in KeycloakAuthenticator using FlashBagAwareSessionInterface.
- Added missing PHPDoc type specifications in WebhookDocuSealController and RgpdService.
- Removed unused MailerService injection in RgpdService.
This commit is contained in:
Serreau Jovann
2026-04-01 17:18:51 +02:00
parent 686de99909
commit 25c593874c
11 changed files with 161 additions and 176 deletions

View File

@@ -5,15 +5,14 @@ namespace App\Controller\Admin;
use App\Entity\Customer;
use App\Entity\User;
use App\Repository\CustomerRepository;
use App\Repository\UserRepository;
use App\Service\MeilisearchService;
use App\Service\UserManagementService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -34,11 +33,10 @@ class ClientsController extends AbstractController
#[Route('/create', name: 'create')]
public function create(
Request $request,
UserRepository $userRepository,
CustomerRepository $customerRepository,
EntityManagerInterface $em,
UserPasswordHasherInterface $passwordHasher,
MeilisearchService $meilisearch,
UserManagementService $userService,
#[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey,
): Response {
if ('POST' === $request->getMethod()) {
@@ -46,29 +44,8 @@ class ClientsController extends AbstractController
$lastName = trim($request->request->getString('lastName'));
$email = trim($request->request->getString('email'));
if ('' === $firstName || '' === $lastName || '' === $email) {
$this->addFlash('error', 'Le prenom, nom et email sont requis.');
return $this->redirectToRoute('app_admin_clients_create');
}
if (null !== $userRepository->findOneBy(['email' => $email])) {
$this->addFlash('error', 'Un compte existe deja avec cet email.');
return $this->redirectToRoute('app_admin_clients_create');
}
try {
$tempPassword = bin2hex(random_bytes(8));
$user = new User();
$user->setEmail($email);
$user->setFirstName($firstName);
$user->setLastName($lastName);
$user->setRoles(['ROLE_CUSTOMER']);
$user->setPassword($passwordHasher->hashPassword($user, $tempPassword));
$user->setTempPassword($tempPassword);
$em->persist($user);
$user = $userService->createBaseUser($email, $firstName, $lastName, ['ROLE_CUSTOMER']);
$customer = new Customer($user);
$customer->setFirstName($firstName);
@@ -120,6 +97,8 @@ class ClientsController extends AbstractController
$this->addFlash('success', 'Client '.$customer->getFullName().' cree.');
return $this->redirectToRoute('app_admin_clients_index');
} catch (\InvalidArgumentException $e) {
$this->addFlash('error', $e->getMessage());
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur : '.$e->getMessage());
}

View File

@@ -5,16 +5,15 @@ namespace App\Controller\Admin;
use App\Entity\Revendeur;
use App\Entity\User;
use App\Repository\RevendeurRepository;
use App\Repository\UserRepository;
use App\Service\MailerService;
use App\Service\MeilisearchService;
use App\Service\UserManagementService;
use Doctrine\ORM\EntityManagerInterface;
use Dompdf\Dompdf;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -38,11 +37,10 @@ class RevendeursController extends AbstractController
public function create(
Request $request,
RevendeurRepository $revendeurRepository,
UserRepository $userRepository,
EntityManagerInterface $em,
UserPasswordHasherInterface $passwordHasher,
MailerService $mailer,
Environment $twig,
UserManagementService $userService,
): Response {
if ('GET' === $request->getMethod()) {
return $this->render('admin/revendeurs/create.html.twig');
@@ -56,32 +54,8 @@ class RevendeursController extends AbstractController
$phone = trim($request->request->getString('phone'));
$isUseStripe = $request->request->getBoolean('isUseStripe');
if ('' === $firstName || '' === $lastName || '' === $email) {
$this->addFlash('error', 'Le prenom, nom et email sont requis.');
return $this->redirectToRoute('app_admin_revendeurs_index');
}
if (null !== $userRepository->findOneBy(['email' => $email])) {
$this->addFlash('error', 'Un compte existe deja avec cet email.');
return $this->redirectToRoute('app_admin_revendeurs_index');
}
try {
// Generer mot de passe temporaire
$tempPassword = bin2hex(random_bytes(8));
// Creer le user
$user = new User();
$user->setEmail($email);
$user->setFirstName($firstName);
$user->setLastName($lastName);
$user->setRoles(['ROLE_REVENDEUR']);
$user->setPassword($passwordHasher->hashPassword($user, $tempPassword));
$user->setTempPassword($tempPassword);
$em->persist($user);
$user = $userService->createBaseUser($email, $firstName, $lastName, ['ROLE_REVENDEUR']);
// Creer le revendeur
$code = $revendeurRepository->generateUniqueCode();
@@ -100,7 +74,7 @@ class RevendeursController extends AbstractController
// Envoyer le mail
$setPasswordUrl = $this->generateUrl('app_set_password', [
'token' => $tempPassword,
'token' => $user->getTempPassword(),
], UrlGeneratorInterface::ABSOLUTE_URL);
$mailer->sendEmail(
@@ -119,6 +93,8 @@ class RevendeursController extends AbstractController
);
$this->addFlash('success', 'Revendeur '.$firstName.' '.$lastName.' cree (Code: '.$code.').');
} catch (\InvalidArgumentException $e) {
$this->addFlash('error', $e->getMessage());
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur lors de la creation : '.$e->getMessage());
}
@@ -183,7 +159,7 @@ class RevendeursController extends AbstractController
#[Route('/{id}/contrat', name: 'contrat', methods: ['GET'])]
public function contrat(
Revendeur $revendeur,
\Twig\Environment $twig,
Environment $twig,
#[Autowire('%kernel.project_dir%')] string $projectDir,
): Response {
$logoPath = $projectDir.'/public/logo.jpg';

View File

@@ -31,6 +31,25 @@ class StatsController extends AbstractController
}
// Donnees fictives
$rawServices = [
['name' => 'Site Internet', 'slug' => 'site-internet', 'color' => '#fabf04', 'ca_ht' => 4_200.00, 'cout' => 150.00, 'clients' => 12, 'abonnements' => 12],
['name' => 'E-Ticket', 'slug' => 'e-ticket', 'color' => '#4338ca', 'ca_ht' => 3_800.00, 'cout' => 0.00, 'clients' => 8, 'abonnements' => 8],
['name' => 'E-Asso', 'slug' => 'e-asso', 'color' => '#16a34a', 'ca_ht' => 1_750.00, 'cout' => 0.00, 'clients' => 7, 'abonnements' => 7],
['name' => 'E-Mail', 'slug' => 'e-mail', 'color' => '#dc2626', 'ca_ht' => 850.00, 'cout' => 70.00, 'clients' => 15, 'abonnements' => 42],
['name' => 'E-Newsletter', 'slug' => 'e-newsletter', 'color' => '#ea580c', 'ca_ht' => 1_100.00, 'cout' => 0.00, 'clients' => 4, 'abonnements' => 4],
['name' => 'E-Sign', 'slug' => 'e-sign', 'color' => '#7c3aed', 'ca_ht' => 400.00, 'cout' => 0.00, 'clients' => 2, 'abonnements' => 2],
['name' => 'E-Ndd', 'slug' => 'e-ndd', 'color' => '#0891b2', 'ca_ht' => 350.00, 'cout' => 0.00, 'clients' => 8, 'abonnements' => 8],
];
$servicesStats = [];
foreach ($rawServices as $s) {
$caTva = $s['ca_ht'] * 0.2;
$servicesStats[] = array_merge($s, [
'ca_tva' => $caTva,
'ca_ttc' => $s['ca_ht'] + $caTva,
]);
}
$globalStats = [
'ca_ht' => 12_450.00,
'ca_tva' => 2_490.00,
@@ -39,85 +58,6 @@ class StatsController extends AbstractController
'commission_stripe' => 186.75,
];
$servicesStats = [
[
'name' => 'Site Internet',
'slug' => 'site-internet',
'color' => '#fabf04',
'ca_ht' => 4_200.00,
'ca_tva' => 840.00,
'ca_ttc' => 5_040.00,
'cout' => 150.00,
'clients' => 12,
'abonnements' => 12,
],
[
'name' => 'E-Ticket',
'slug' => 'e-ticket',
'color' => '#4338ca',
'ca_ht' => 3_800.00,
'ca_tva' => 760.00,
'ca_ttc' => 4_560.00,
'cout' => 0.00,
'clients' => 8,
'abonnements' => 8,
],
[
'name' => 'E-Asso',
'slug' => 'e-asso',
'color' => '#16a34a',
'ca_ht' => 1_750.00,
'ca_tva' => 350.00,
'ca_ttc' => 2_100.00,
'cout' => 0.00,
'clients' => 7,
'abonnements' => 7,
],
[
'name' => 'E-Mail',
'slug' => 'e-mail',
'color' => '#dc2626',
'ca_ht' => 850.00,
'ca_tva' => 170.00,
'ca_ttc' => 1_020.00,
'cout' => 70.00,
'clients' => 15,
'abonnements' => 42,
],
[
'name' => 'E-Newsletter',
'slug' => 'e-newsletter',
'color' => '#ea580c',
'ca_ht' => 1_100.00,
'ca_tva' => 220.00,
'ca_ttc' => 1_320.00,
'cout' => 0.00,
'clients' => 4,
'abonnements' => 4,
],
[
'name' => 'E-Sign',
'slug' => 'e-sign',
'color' => '#7c3aed',
'ca_ht' => 400.00,
'ca_tva' => 80.00,
'ca_ttc' => 480.00,
'cout' => 0.00,
'clients' => 2,
'abonnements' => 2,
],
[
'name' => 'E-Ndd',
'slug' => 'e-ndd',
'color' => '#0891b2',
'ca_ht' => 350.00,
'ca_tva' => 70.00,
'ca_ttc' => 420.00,
'clients' => 8,
'abonnements' => 8,
],
];
// Evolution mensuelle fictive (6 derniers mois)
$monthlyEvolution = [
['month' => 'Oct 2025', 'ca_ht' => 9_800],

View File

@@ -5,6 +5,7 @@ namespace App\Controller\Admin;
use App\Entity\Service;
use App\Entity\ServiceCategory;
use App\Entity\ServiceMessage;
use App\Entity\User;
use App\Repository\ServiceCategoryRepository;
use App\Repository\ServiceRepository;
use Doctrine\ORM\EntityManagerInterface;
@@ -203,7 +204,12 @@ class StatusController extends AbstractController
return $this->redirectToRoute('app_admin_status_index');
}
$message = new ServiceMessage($service, $title, $content, $severity, $this->getUser());
$user = $this->getUser();
if (!$user instanceof User) {
$user = null;
}
$message = new ServiceMessage($service, $title, $content, $severity, $user);
$em->persist($message);
$em->flush();

View File

@@ -53,7 +53,7 @@ class EmailTrackingController extends AbstractController
$attachmentHtml .= '<tr><td style="padding: 12px 0 8px; font-size: 12px; font-weight: bold; text-transform: uppercase; color: #666; letter-spacing: 1px;">Pieces jointes</td></tr>';
foreach ($attachments as $index => $attachment) {
$name = $attachment['name'] ?? basename($attachment['path']);
$name = $attachment['name'];
$downloadUrl = $this->generateUrl('app_email_attachment', [
'messageId' => $messageId,
'index' => $index,
@@ -91,7 +91,7 @@ class EmailTrackingController extends AbstractController
$attachment = $attachments[$index];
$path = $attachment['path'];
$name = $attachment['name'] ?? basename($path);
$name = $attachment['name'];
if (!file_exists($path)) {
throw $this->createNotFoundException('Le fichier n\'est plus disponible.');

View File

@@ -113,6 +113,9 @@ class WebhookDocuSealController extends AbstractController
return new JsonResponse(['status' => 'ok', 'event' => 'started', 'reference' => $attestation->getReference()]);
}
/**
* @param array<string, mixed> $data
*/
private function handleAttestationCompleted(
array $data,
AttestationRepository $attestationRepository,
@@ -171,6 +174,9 @@ class WebhookDocuSealController extends AbstractController
return new JsonResponse(['status' => 'ok', 'event' => 'completed', 'reference' => $attestation->getReference()]);
}
/**
* @param array<string, mixed> $data
*/
private function downloadDocumentsFromWebhook(array $data, Attestation $attestation, string $projectDir): void
{
$dir = $projectDir.'/var/rgpd/signed';
@@ -204,6 +210,9 @@ class WebhookDocuSealController extends AbstractController
}
}
/**
* @param array<string, mixed> $data
*/
private function handleAttestationDeclined(
array $data,
AttestationRepository $attestationRepository,

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Repository;
use App\Entity\MessengerLog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MessengerLog>
*/
class MessengerLogRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MessengerLog::class);
}
}

View File

@@ -10,6 +10,7 @@ use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
@@ -82,7 +83,10 @@ class KeycloakAuthenticator extends OAuth2Authenticator implements Authenticatio
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->getFlashBag()->add('error', 'Echec de la connexion E-Cosplay. Veuillez reessayer.');
$session = $request->getSession();
if ($session instanceof FlashBagAwareSessionInterface) {
$session->getFlashBag()->add('error', 'Echec de la connexion E-Cosplay. Veuillez reessayer.');
}
return new RedirectResponse($this->router->generate('app_home'));
}

View File

@@ -76,9 +76,16 @@ class MailerService
}
if ($attachments) {
$processedAttachments = [];
foreach ($attachments as $attachment) {
$email->attachFromPath($attachment['path'], $attachment['name'] ?? null);
$name = $attachment['name'] ?? basename($attachment['path']);
$email->attachFromPath($attachment['path'], $name);
$processedAttachments[] = [
'path' => $attachment['path'],
'name' => $name,
];
}
$attachments = $processedAttachments;
}
$messageId = bin2hex(random_bytes(16));

View File

@@ -21,7 +21,6 @@ class RgpdService
public function __construct(
private EntityManagerInterface $em,
private Environment $twig,
private MailerService $mailer,
private DocuSealService $docuSealService,
private UrlGeneratorInterface $urlGenerator,
#[Autowire('%kernel.project_dir%')] private string $projectDir,
@@ -58,19 +57,10 @@ class RgpdService
*/
public function handleAccessRequest(string $ip, string $email): array
{
$ipHash = hash('sha256', $ip);
$visitors = $this->em->getRepository(AnalyticsUniqId::class)->findBy(['ipHash' => $ipHash]);
$visitors = $this->getVisitorsByIp($ip);
if (0 === \count($visitors)) {
$attestation = new Attestation('no_data', $ip, $email, $this->hmacSecret);
$this->em->persist($attestation);
$this->em->flush();
$pdfPath = $this->generateNoDataPdf($ip, $attestation);
$attestation->setPdfFileUnsigned($pdfPath);
$this->em->flush();
$this->docuSealService->signAttestation($attestation);
$this->createAndSignAttestation('no_data', $ip, $email);
return ['found' => false, 'count' => 0];
}
@@ -88,15 +78,7 @@ class RgpdService
];
}
$attestation = new Attestation('access', $ip, $email, $this->hmacSecret);
$this->em->persist($attestation);
$this->em->flush();
$pdfPath = $this->generateAccessPdf($data, $ip, $attestation);
$attestation->setPdfFileUnsigned($pdfPath);
$this->em->flush();
$this->docuSealService->signAttestation($attestation);
$this->createAndSignAttestation('access', $ip, $email, $data);
return ['found' => true, 'count' => \count($visitors)];
}
@@ -106,19 +88,10 @@ class RgpdService
*/
public function handleDeletionRequest(string $ip, string $email): array
{
$ipHash = hash('sha256', $ip);
$visitors = $this->em->getRepository(AnalyticsUniqId::class)->findBy(['ipHash' => $ipHash]);
$visitors = $this->getVisitorsByIp($ip);
if (0 === \count($visitors)) {
$attestation = new Attestation('no_data', $ip, $email, $this->hmacSecret);
$this->em->persist($attestation);
$this->em->flush();
$pdfPath = $this->generateNoDataPdf($ip, $attestation);
$attestation->setPdfFileUnsigned($pdfPath);
$this->em->flush();
$this->docuSealService->signAttestation($attestation);
$this->createAndSignAttestation('no_data', $ip, $email);
return ['found' => false, 'deleted' => 0];
}
@@ -129,17 +102,40 @@ class RgpdService
$this->em->remove($visitor);
}
$attestation = new Attestation('deletion', $ip, $email, $this->hmacSecret);
$this->createAndSignAttestation('deletion', $ip, $email);
return ['found' => true, 'deleted' => $count];
}
/**
* @return list<AnalyticsUniqId>
*/
private function getVisitorsByIp(string $ip): array
{
$ipHash = hash('sha256', $ip);
return $this->em->getRepository(AnalyticsUniqId::class)->findBy(['ipHash' => $ipHash]);
}
private function createAndSignAttestation(string $type, string $ip, string $email, mixed $data = null): Attestation
{
$attestation = new Attestation($type, $ip, $email, $this->hmacSecret);
$this->em->persist($attestation);
$this->em->flush();
$pdfPath = $this->generateDeletionPdf($ip, $attestation);
$pdfPath = match ($type) {
'no_data' => $this->generateNoDataPdf($ip, $attestation),
'access' => $this->generateAccessPdf($data, $ip, $attestation),
'deletion' => $this->generateDeletionPdf($ip, $attestation),
default => throw new \InvalidArgumentException('Invalid attestation type'),
};
$attestation->setPdfFileUnsigned($pdfPath);
$this->em->flush();
$this->docuSealService->signAttestation($attestation);
return ['found' => true, 'deleted' => $count];
return $attestation;
}
private function getLogoBase64(): string
@@ -149,6 +145,9 @@ class RgpdService
return file_exists($logoPath) ? 'data:image/jpeg;base64,'.base64_encode((string) file_get_contents($logoPath)) : '';
}
/**
* @return array{ip: string, logo: string, date: \DateTimeImmutable, attestation: Attestation, qrcode: string, verify_url: string}
*/
private function getPdfVars(string $ip, Attestation $attestation): array
{
return [

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserManagementService
{
public function __construct(
private UserRepository $userRepository,
private EntityManagerInterface $em,
private UserPasswordHasherInterface $passwordHasher,
) {
}
/**
* @param string[] $roles
* @throws \Exception
*/
public function createBaseUser(string $email, string $firstName, string $lastName, array $roles): User
{
if ('' === $email || '' === $firstName || '' === $lastName) {
throw new \InvalidArgumentException('Le prenom, nom et email sont requis.');
}
if (null !== $this->userRepository->findOneBy(['email' => $email])) {
throw new \Exception('Un compte existe deja avec cet email.');
}
$tempPassword = bin2hex(random_bytes(8));
$user = new User();
$user->setEmail($email);
$user->setFirstName($firstName);
$user->setLastName($lastName);
$user->setRoles($roles);
$user->setPassword($this->passwordHasher->hashPassword($user, $tempPassword));
$user->setTempPassword($tempPassword);
$this->em->persist($user);
return $user;
}
}