feat(various): Refactorise et améliore plusieurs services et entités.

Supprime des fichiers inutilisés, ajoute du typage, gère les exceptions,
sécurise la génération de mots de passe et améliore la gestion des logs.
This commit is contained in:
Serreau Jovann
2025-09-27 17:09:03 +02:00
parent 8de33aae58
commit bad2d6b95c
24 changed files with 719 additions and 577 deletions

View File

@@ -12,47 +12,54 @@ class SignClient
{
private \Docuseal\Api $docuseal;
public function __construct(private readonly RequestStack $requestStack,private readonly UploaderHelper $uploaderHelper,private readonly UrlGeneratorInterface $urlGenerator,private readonly EntityManagerInterface $entityManager)
{
$this->docuseal = new \Docuseal\Api($_ENV['DOCUSIGN_KEY'], $_ENV['DOCUSIGN_URL']);
public function __construct(
private readonly RequestStack $requestStack,
private readonly UploaderHelper $uploaderHelper,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly EntityManagerInterface $entityManager
) {
$key = $_ENV['DOCUSIGN_KEY'] ?? '';
$url = $_ENV['DOCUSIGN_URL'] ?? '';
$this->docuseal = new \Docuseal\Api($key, $url);
}
public function createSubmissionDevis(CustomerDevis $devis)
public function createSubmissionDevis(CustomerDevis $devis): string
{
if ($devis->getDevisSubmiterId() === null) {
$currentRequest = $this->requestStack->getCurrentRequest();
if ($currentRequest === null) {
throw new \RuntimeException('No current request available');
}
$t = new \DateTimeImmutable();
if($devis->getDevisSubmiterId() == null) {
$submissionId = $this->docuseal->createSubmissionFromPdf([
'name' => 'Devis N°'.$devis->getNumDevis(),
'completed_redirect_url' => $this->urlGenerator->generate('app_sign_complete',['type'=>'devis','id'=>$devis->getId()],UrlGeneratorInterface::ABSOLUTE_URL),
'send_email' => false,
'documents' => [
[
'name' => 'devis',
'file' => $this->requestStack->getCurrentRequest()->getSchemeAndHttpHost().$this->uploaderHelper->asset($devis,'devis'),
]
],
'submitters' => [
$submissionId = $this->docuseal->createSubmissionFromPdf([
'name' => 'Devis N°' . $devis->getNumDevis(),
'completed_redirect_url' => $this->urlGenerator->generate(
'app_sign_complete',
['type' => 'devis', 'id' => $devis->getId()],
UrlGeneratorInterface::ABSOLUTE_URL
),
'send_email' => false,
'documents' => [
[
'name' => 'devis',
'file' => $currentRequest->getSchemeAndHttpHost() . $this->uploaderHelper->asset($devis, 'devis'),
],
],
'submitters' => [
[
'role' => 'First Party',
'email' => $devis->getCustomer()->mainContact()->getEmail(),
]
]
]);
],
],
]);
$devis->setDevisSubmiterId($submissionId['id']);
$this->entityManager->persist($devis);
$this->entityManager->flush();
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
} else {
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
$devis->setDevisSubmiterId($submissionId['id']);
$this->entityManager->persist($devis);
$this->entityManager->flush();
}
return "https://signature.esy-web.dev/s/".$submissionData['slug'];
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
return "https://signature.esy-web.dev/s/" . $submissionData['slug'];
}
}

View File

@@ -1,72 +1,52 @@
<?php
namespace App\Service\Generator;
/**
* Class TempPasswordGenerator
*
* Provides functionality to generate secure temporary passwords.
* Fournit des fonctionnalités pour générer des mots de passe temporaires sécurisés.
*/
class TempPasswordGenerator
{
private const DEFAULT_LENGTH = 12;
private const DEFAULT_CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_=+[]{}|;:,.<>?';
/**
* Generates a random temporary password.
* Génère un mot de passe temporaire aléatoire.
*
* @param int $length The desired length of the password. Default is 12 characters.
* @param string $characters A string of characters to use for password generation.
* Defaults to a mix of uppercase, lowercase, numbers, and symbols.
* @return string The generated temporary password.
* @param int $length Longueur désirée du mot de passe (par défaut 12).
* @param string $characters Jeu de caractères à utiliser dans le mot de passe.
* @return string Mot de passe généré.
*/
public static function generate(int $length = 12, string $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_=+[]{}|;:,.<>?') : string
public static function generate(int $length = self::DEFAULT_LENGTH, string $characters = self::DEFAULT_CHARACTERS): string
{
// Ensure the length is positive
if ($length <= 0) {
// You might want to throw an exception or return an empty string
// depending on how you want to handle invalid lengths.
// For simplicity, we'll default to 12 if an invalid length is provided.
$length = 12;
$length = self::DEFAULT_LENGTH;
}
$password = '';
$charactersLength = strlen($characters);
$maxIndex = strlen($characters) - 1;
// Generate the password character by character
for ($i = 0; $i < $length; $i++) {
// Use random_int for cryptographically secure random number generation
$password .= $characters[random_int(0, $charactersLength - 1)];
$password .= $characters[random_int(0, $maxIndex)];
}
return $password;
}
/**
* Checks if a password meets certain complexity requirements (optional).
* This is a basic example and can be extended.
* Vérifie si un mot de passe remplit certaines exigences de complexité.
*
* @param string $password The password to check.
* @return bool True if the password meets basic complexity, false otherwise.
* @param string $password Mot de passe à vérifier.
* @return bool True si les critères sont respectés, false sinon.
*/
public static function isComplex(string $password): bool
{
// Minimum length
if (strlen($password) < 8) {
return false;
}
// Requires at least one uppercase letter
if (!preg_match('/[A-Z]/', $password)) {
return false;
}
// Requires at least one lowercase letter
if (!preg_match('/[a-z]/', $password)) {
return false;
}
// Requires at least one number
if (!preg_match('/[0-9]/', $password)) {
return false;
}
// Requires at least one special character
if (!preg_match('/[!@#$%^&*()-_=+\[\]{}|;:,.<>?]/', $password)) {
return false;
}
return true;
return strlen($password) >= 8
&& preg_match('/[A-Z]/', $password)
&& preg_match('/[a-z]/', $password)
&& preg_match('/[0-9]/', $password)
&& preg_match('/[!@#$%^&*()\-_=+\[\]{}|;:,.<>?]/', $password);
}
}

View File

@@ -10,7 +10,6 @@ use Google\Cloud\Compute\V1\GetInstanceRequest;
use Google\Cloud\Compute\V1\Instance;
use Google\Cloud\Compute\V1\ListInstancesRequest;
use Google\Cloud\Compute\V1\NetworkInterface;
use Google\Protobuf\RepeatedField;
use Symfony\Component\HttpKernel\KernelInterface;
class ComputeEngineClient
@@ -19,59 +18,83 @@ class ComputeEngineClient
private string $projectId;
private string $zone;
public function __construct(private readonly EntityManagerInterface $entityManager,KernelInterface $kernel)
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
KernelInterface $kernel
) {
$credentialsPath = $kernel->getProjectDir() . "/account.json";
$this->client = new InstancesClient([
'credentials' => $kernel->getProjectDir()."/account.json"
'credentials' => $credentialsPath,
]);
$content = file_get_contents($kernel->getProjectDir()."/account.json");
$content = json_decode($content);
$this->projectId = $content->project_id;
$this->zone = "europe-west4-a";
$content = json_decode(file_get_contents($credentialsPath), false);
$this->projectId = $content->project_id ?? '';
$this->zone = 'europe-west4-a';
}
/**
* Liste les instances compute commençant par 'srv-' en base ou en création.
*
* @return Compute[]
*/
public function list(): array
{
$request = (new ListInstancesRequest())
$request = (new ListInstancesRequest())
->setProject($this->projectId)
->setZone($this->zone);
$instancesList = $this->client->list($request);
$instances = [];
/** @var Instance $instance */
foreach ($instancesList as $instance) {
if(str_contains($instance->getName(),'srv-')) {
if (str_contains($instance->getName(), 'srv-')) {
/** @var NetworkInterface $network */
$network = $instance->getNetworkInterfaces()[0];
/** @var AccessConfig $accessConfig */
$accessConfig = $network->getAccessConfigs()[0];
$compute = $this->entityManager->getRepository(Compute::class)->findOneBy(['instanceId'=>$instance->getId()]);
if(!$compute instanceof Compute) {
$compute = $this->entityManager->getRepository(Compute::class)
->findOneBy(['instanceId' => $instance->getId()]);
if (!$compute instanceof Compute) {
$compute = new Compute();
$compute->setInstanceId($instance->getId());
$compute->setZone(str_replace("https://www.googleapis.com/compute/v1/projects/".$this->projectId."/zones/","",$instance->getZone()));
$compute->setZone(str_replace(
"https://www.googleapis.com/compute/v1/projects/{$this->projectId}/zones/",
'',
$instance->getZone()
));
$compute->setInternalIp($network->getNetworkIP());
$compute->setExternalIp($accessConfig->getNatIP());
$compute->setStatus("down");
$compute->setStatus('down');
}
$this->entityManager->persist($compute);
$instances[] = $compute;
}
}
$this->entityManager->flush();
return $instances;
}
public function detail(Compute $compute)
/**
* Charge les détails dune instance Compute et met à jour le statut.
*/
public function detail(Compute $compute): Compute
{
$request = (new GetInstanceRequest())
->setInstance($compute->getInstanceId())
->setProject($this->projectId)
->setZone($this->zone);
$instance = $this->client->get($request);
$compute->setStatus($instance->getStatus());
$compute->name = $instance->getName();
return$compute;
return $compute;
}
}

View File

@@ -8,26 +8,23 @@ use App\Service\Mailer\Mailer;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class LoggerEventListener
{
private LoggerInterface $logger;
public function __construct(private Mailer $mailer,private readonly TokenStorageInterface $tokenStorage,LoggerInterface $logger)
{
$this->logger = $logger;
public function __construct(
private readonly Mailer $mailer,
private readonly TokenStorageInterface $tokenStorage,
private readonly LoggerInterface $logger
) {
}
public function postPersist(Logger $logger, LifecycleEventArgs $args): void
{
try {
$logger->lock();
} catch (ImmutableLoggerFieldException $e) {
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postPersist) : ' . $e->getMessage());
} catch (ImmutableLoggerFieldException $exception) {
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postPersist) : ' . $exception->getMessage());
}
}
@@ -35,17 +32,22 @@ class LoggerEventListener
{
try {
$logger->lock();
} catch (ImmutableLoggerFieldException $e) {
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postLoad) : ' . $e->getMessage());
} catch (ImmutableLoggerFieldException $exception) {
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postLoad) : ' . $exception->getMessage());
}
}
#[AsEventListener(event: ImmutableLoggerFieldException::class)]
public function onKernelException(ImmutableLoggerFieldException $event): void {
$account = $this->tokenStorage->getToken();
$account = $account->getUser();
$this->mailer->sendMulti(["jovann@siteconseil.fr","legrand@siteconseil.fr"],"[Mainframe] - Tentative de modifier du log ! ","mails/artemis/error-logger.twig",[
'account' => $account,
]);
public function onKernelException(ImmutableLoggerFieldException $exception): void
{
$token = $this->tokenStorage->getToken();
$account = $token ? $token->getUser() : null;
$this->mailer->sendMulti(
['jovann@siteconseil.fr', 'legrand@siteconseil.fr'],
'[Mainframe] - Tentative de modifier du log !',
'mails/artemis/error-logger.twig',
['account' => $account]
);
}
}

View File

@@ -14,34 +14,45 @@ class LoggerService
public function __construct(
private readonly EntityManagerInterface $em,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly VaultClient $vaultClient,
) {}
private readonly VaultClient $vaultClient
) {
}
public function log(string $type, string $content, ?Account $account = null): Logger
{
$entryAt = new \DateTimeImmutable();
$uuid = Uuid::v4();
$logger = new Logger();
$logger->setEvent($this->eventDispatcher);
$logger->setEntryAt($entryAt)
->setType($this->vaultClient->encrypt("mainframe_logger",$type))
->setContent($this->vaultClient->encrypt("mainframe_logger",$content))
->setType($this->vaultClient->encrypt('mainframe_logger', $type))
->setContent($this->vaultClient->encrypt('mainframe_logger', $content))
->setAccount($account)
->setUuid($uuid);
$hmac = $this->generateHmac($logger);
$logger->setHmac($this->vaultClient->encrypt("mainframe_logger",$hmac));
$logger->setHmac($this->vaultClient->encrypt('mainframe_logger', $hmac));
$this->em->persist($logger);
$this->em->flush();
return $logger;
}
public function isLoggerTampered(Logger $logger): bool
{
$expectedHmac = $this->generateHmac($logger);
return !hash_equals($expectedHmac, $logger->getHmac());
$actualHmac = $logger->getHmac();
// Décrypter le HMAC stocké pour comparaison
$decryptedHmac = $this->vaultClient->decrypt('mainframe_logger', (string) $actualHmac);
if ($decryptedHmac === null) {
return true;
}
return !hash_equals($expectedHmac, $decryptedHmac);
}
private function generateHmac(Logger $logger): string
@@ -49,17 +60,24 @@ class LoggerService
$secretKey = $_ENV['APP_SECRET'] ?? 'change_this_secret';
$data = implode('|', [
$logger->getType(),
$logger->getContent(),
$logger->getEntryAt()?->format(DATE_ATOM),
$logger->getAccount()?->getId(),
$logger->getUuid()?->toRfc4122(),
$logger->getType() ?? '',
$logger->getContent() ?? '',
$logger->getEntryAt()?->format(DATE_ATOM) ?? '',
$logger->getAccount()?->getId() ?? '',
$logger->getUuid()?->toRfc4122() ?? '',
]);
return hash_hmac('sha256', $data, $secretKey);
}
public function load(Account $account)
/**
* Charge les logs pour un compte donné.
* Typage ajouté.
*
* @param Account $account
* @return Logger[]
*/
public function load(Account $account): array
{
return $this->em->getRepository(Logger::class)->load($account);
}

View File

@@ -3,48 +3,48 @@
namespace App\Service\Mailer;
use App\Entity\Mail;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Header\IdentificationHeader;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
class AmazonSesClient
{
private Mailer $mailer;
public function __construct(
private UrlGeneratorInterface $urlGenerator,
EventDispatcherInterface $eventDispatcher
) {
$transport = new EsmtpTransport(
'email-smtp.eu-west-3.amazonaws.com',
587,
false,
$eventDispatcher
);
$transport->setUsername($_ENV['AMAZON_SES_PUBLIC'] ?? '');
$transport->setPassword($_ENV['AMAZON_SES_SECRET'] ?? '');
private \Symfony\Component\Mailer\Mailer $mailer;
public function __construct(private UrlGeneratorInterface $urlGenerator,EventDispatcherInterface $eventDispatcher)
{
//init ses client
$transport = new EsmtpTransport("email-smtp.eu-west-3.amazonaws.com",587,false,$eventDispatcher);
$transport->setUsername($_ENV['AMAZON_SES_PUBLIC']);
$transport->setPassword($_ENV['AMAZON_SES_SECRET']);
$this->mailer = new \Symfony\Component\Mailer\Mailer($transport);
$this->mailer = new Mailer($transport);
}
public function send(string $html,string $address,string $subject)
/**
* Envoie un email HTML avec tracking invisible.
*/
public function send(string $html, string $address, string $subject): void
{
$email = new Email();
$email->from('no-reply@siteconseil.fr');
$email->to($address);
$email->subject($subject);
$email = (new Email())
->from('no-reply@siteconseil.fr')
->to($address)
->subject($subject);
// Génération et ajout de l'en-tête Message-Id
$messageId = $email->generateMessageId();
$header = $email->getHeaders();
$header->add(new IdentificationHeader("Message-Id",$messageId));
$datas = $this->generateTracking($email);
$email->getHeaders()->add(new IdentificationHeader('Message-Id', $messageId));
$tracking = $this->generateTracking($email);
$dom = new \DOMDocument();
libxml_use_internal_errors(true);
@@ -52,35 +52,42 @@ class AmazonSesClient
libxml_clear_errors();
$img = $dom->createElement('img');
$img->setAttribute('src', $datas['url']);
$img->setAttribute('width',0);
$img->setAttribute('height',0);
$img->setAttribute('src', $tracking['url']);
$img->setAttribute('width', '0');
$img->setAttribute('height', '0');
$body = $dom->getElementsByTagName('body')->item(0);
if ($body) {
// Ajouter <img> avant la fin de </body>
$body->appendChild($img);
}
$newHtml = $dom->saveHTML();
$email->html($newHtml);
$this->mailer->send($email);
$email->html($newHtml);
$this->mailer->send($email);
}
private function generateTracking(Email $email)
/**
* Génère les données de tracking et lURL associée.
*
* @param Email $email
* @return array{object: Mail, url: string}
*/
private function generateTracking(Email $email): array
{
$messageFormat = $email->getHeaders()->get('message-id')->getBody()[0];
$messageFormat = str_replace("@siteconseil.fr","",$messageFormat);
$messageIdHeader = $email->getHeaders()->get('message-id');
$messageFormat = $messageIdHeader ? $messageIdHeader->getBody()[0] : '';
$messageFormat = str_replace('@siteconseil.fr', '', $messageFormat);
$mailData = new Mail();
$mailData->setDest($email->getTo()[0]->getAddress());
$mailData->setSubject($email->getSubject());
$mailData->setMessageId($messageFormat);
$mailData->setStatus("draft");
$mailData->setStatus('draft');
return [
'object' => $mailData,
'url'=> "https://mainframe.esy-web.dev".$this->urlGenerator->generate('app_tracking',['slug'=>$messageFormat])
'url' => 'https://mainframe.esy-web.dev' . $this->urlGenerator->generate('app_tracking', ['slug' => $messageFormat]),
];
}
}

View File

@@ -1,30 +1,22 @@
<?php
namespace App\Service\Mailer\Event;
use App\Entity\Account;
class CreatedAdminEvent
{
private Account $account;
private string $password;
public function __construct(Account $account, string $password)
{
$this->account = $account;
$this->password = $password;
public function __construct(
private readonly Account $account,
private readonly string $password
) {
}
/**
* @return Account
*/
public function getAccount(): Account
{
return $this->account;
}
/**
* @return string
*/
public function getPassword(): string
{
return $this->password;

View File

@@ -19,163 +19,199 @@ use Twig\Environment;
class Mailer
{
private readonly MailerInterface $mailer;
private \Symfony\Component\Mailer\Mailer $mailer;
public function __construct(MailerInterface $mailer,private readonly EntityManagerInterface $entityManager,private readonly UrlGeneratorInterface $urlGenerator,private readonly ?Profiler $profiler,private readonly Environment $environment)
{
public function __construct(
MailerInterface $mailer,
private readonly EntityManagerInterface $entityManager,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly ?Profiler $profiler,
private readonly Environment $environment,
) {
$this->mailer = $mailer;
}
private function convertMjmlToHtml(string $mjmlContent): string
{
$command = ['mjml', '--stdin'];
$process = new Process($command);
$process = new Process(['mjml', '--stdin']);
$process->setInput($mjmlContent);
try {
$process->setInput($mjmlContent);
$process->run();
// Exécute la commande et vérifie la réussite
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process->getOutput();
} catch (ProcessFailedException $exception) {
return ''; // Retourne une chaîne vide en cas d'échec
} catch (Exception $e) {
return '';
} catch (ProcessFailedException|Exception) {
return ''; // Retourne vide en cas d'échec
}
}
public function sendTest()
public function sendTest(): void
{
$dest = new Address("test-dev@siteconseil.fr", "Test Dev");
$src = new Address("mainframe@esy-web.dev", "Mainframe EsyWeb");
$mail = new Email();
$mail->subject("Test de configuration");
$mail->to($dest);
$mail->from($src);
$mail = (new Email())
->subject("Test de configuration")
->to($dest)
->from($src);
$messageId = $mail->generateMessageId();
$header = $mail->getHeaders();
$header->add(new IdentificationHeader("Message-Id",$messageId));
$mail->getHeaders()->add(new IdentificationHeader("Message-Id", $messageId));
$datas = $this->generateTracking($mail);
/** @var Mail $object */
$object= $datas['object'];
$mjmlGenerator = $this->environment->render('mails/test.twig',[
$object = $datas['object'];
$mjmlGenerator = $this->environment->render('mails/test.twig', [
'system' => [
'subject' => 'Test de configuration',
'tracking_url'=>$datas['url'],
]
'tracking_url' => $datas['url'],
],
]);
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
$object->setContent($htmlContent);
$mail->html($htmlContent);
try {
$this->mailer->send($mail);
$object->setStatus("sent");
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
$object->setStatus("error");
}
$this->entityManager->persist($object);
$this->entityManager->flush();
}
public function send(string $address, string $addressName, string $subject, string $template, array $data,array $files = [])
{
/**
* @param string $address
* @param string $addressName
* @param string $subject
* @param string $template
* @param array<string, mixed> $data
* @param array<\Symfony\Component\Mime\Part\Part> $files
*/
public function send(
string $address,
string $addressName,
string $subject,
string $template,
array $data,
array $files = []
): void {
$dest = new Address($address, $addressName);
$src = new Address("mainframe@esy-web.dev", "Mainframe EsyWeb");
$mail = new Email();
$mail->subject($subject);
$mail->to($dest);
$mail->from($src);
$mail = (new Email())
->subject($subject)
->to($dest)
->from($src);
$messageId = $mail->generateMessageId();
$header = $mail->getHeaders();
$header->add(new IdentificationHeader("Message-Id",$messageId));
$mail->getHeaders()->add(new IdentificationHeader("Message-Id", $messageId));
$datasSign = $this->generateTracking($mail);
/** @var Mail $object */
$object = $datasSign['object'];
$mjmlGenerator = $this->environment->render($template, [
'system' => [
'subject' => $subject,
'tracking_url'=>$datasSign['url']
'tracking_url' => $datasSign['url'],
],
'datas' => $data,
]);
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
$object->setContent($htmlContent);
foreach ($files as $file) {
$mail->addPart($file);
$mail->addPart($file);
}
$mail->html($htmlContent);
try {
$this->mailer->send($mail);
$object->setStatus("sent");
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
$object->setStatus("error");
}
$this->entityManager->persist($object);
$this->entityManager->flush();
}
private function generateTracking(Email $email)
/**
* @param Email $email
* @return array{object: Mail, url: string}
*/
private function generateTracking(Email $email): array
{
$messageFormat = $email->getHeaders()->get('message-id')->getBody()[0];
$messageFormat = str_replace("@esy-web.dev","",$messageFormat);
$messageIdHeader = $email->getHeaders()->get('message-id');
$messageFormat = $messageIdHeader ? $messageIdHeader->getBody()[0] : '';
$messageFormat = str_replace("@esy-web.dev", "", $messageFormat);
$mailData = new Mail();
$mailData->setDest($email->getTo()[0]->getAddress());
$mailData->setSubject($email->getSubject());
$mailData->setMessageId($messageFormat);
$mailData->setStatus("draft");
return [
'object' => $mailData,
'url'=> "https://mainframe.esy-web.dev".$this->urlGenerator->generate('app_tracking',['slug'=>$messageFormat])
];
return [
'object' => $mailData,
'url' => "https://mainframe.esy-web.dev" . $this->urlGenerator->generate('app_tracking', ['slug' => $messageFormat]),
];
}
public function sendMulti(array $addressList, string $subject, string $template, array $data)
/**
* @param string[] $addressList
* @param string $subject
* @param string $template
* @param array<string, mixed> $data
*/
public function sendMulti(array $addressList, string $subject, string $template, array $data): void
{
$src = new Address("mainframe@esy-web.dev", "Mainframe EsyWeb");
$mail = new Email();
$mail->subject($subject);
$mail = (new Email())->subject($subject);
foreach ($addressList as $address) {
$dest = new Address($address);
$mail->addTo($dest);
}
$mail->from($src);
$messageId = $mail->generateMessageId();
$header = $mail->getHeaders();
$header->add(new IdentificationHeader("Message-Id",$messageId));
$mail->getHeaders()->add(new IdentificationHeader("Message-Id", $messageId));
$datasSign = $this->generateTracking($mail);
/** @var Mail $object */
$object = $datasSign['object'];
$mjmlGenerator = $this->environment->render($template, [
'system' => [
'subject' => $subject,
'tracking_url'=>$datasSign['url']
'tracking_url' => $datasSign['url'],
],
'datas' => $data,
]);
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
$object->setContent($htmlContent);
$mail->html($htmlContent);
try {
$this->mailer->send($mail);
$object->setStatus("sent");
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
$object->setStatus("error");
}
$this->entityManager->persist($object);
$this->entityManager->flush();
}

View File

@@ -1,4 +1,5 @@
<?php
namespace App\Service\Mailer;
use App\Service\Mailer\Event\CreatedAdminEvent;
@@ -8,19 +9,27 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsEventListener(event: CreatedAdminEvent::class, method: 'onAdminEvent')]
class MailerSubscriber
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator,private readonly Mailer $mailer)
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly Mailer $mailer
) {
}
public function onAdminEvent(CreatedAdminEvent $createdAdminEvent)
public function onAdminEvent(CreatedAdminEvent $createdAdminEvent): void
{
$account = $createdAdminEvent->getAccount();
$password = $createdAdminEvent->getPassword();
$this->mailer->send($account->getEmail(), $account->getUsername(), "[MainFrame] - Création d'un compte administrateur", "mails/artemis/new_admin.twig", [
'username' => $account->getUsername(),
'password' => $password,
'url' => $this->urlGenerator->generate('app_login',[],UrlGeneratorInterface::ABSOLUTE_URL)
]);
$this->mailer->send(
$account->getEmail(),
$account->getUsername(),
"[MainFrame] - Création d'un compte administrateur",
"mails/artemis/new_admin.twig",
[
'username' => $account->getUsername(),
'password' => $password,
'url' => $this->urlGenerator->generate('app_login', [], UrlGeneratorInterface::ABSOLUTE_URL),
]
);
}
}

View File

@@ -11,25 +11,37 @@ class Client
public function __construct()
{
$this->ovhClient = new Api(
$_ENV['OVH_KEY'],
$_ENV['OVH_SECRET'],
"ovh-eu",
$_ENV['OVH_CUSTOMER']
$_ENV['OVH_KEY'] ?? '',
$_ENV['OVH_SECRET'] ?? '',
'ovh-eu',
$_ENV['OVH_CUSTOMER'] ?? ''
);
}
/**
* Récupère les informations du domaine OVH.
*
* @param string|null $ndd Nom de domaine
* @return array|null Tableau avec date d'expiration et serveurs DNS ou null en cas d'erreur / domaine vide
*/
public function info(?string $ndd): ?array
{
if (empty($ndd)) {
return null;
}
try {
$ndd = $this->ovhClient->get('/domain/' . $ndd);
$domainInfo = $this->ovhClient->get('/domain/' . $ndd);
return [
'expired' => $ndd['expirationDate'],
'expired' => $domainInfo['expirationDate'] ?? null,
'nameServer' => [
0 => $ndd['nameServers'][0]['nameServer'],
1 => $ndd['nameServers'][1]['nameServer'],
]
$domainInfo['nameServers'][0]['nameServer'] ?? null,
$domainInfo['nameServers'][1]['nameServer'] ?? null,
],
];
} catch (\Exception $e) {
} catch (\Throwable $e) {
// Loger l'erreur si besoin ou gérer autrement
return null;
}
}

View File

@@ -11,148 +11,145 @@ define('EURO', chr(128));
class DevisPdf extends FPDF
{
private $items;
private array $items;
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerDevis $customerDevis)
{
public function __construct(
private readonly KernelInterface $kernel,
private readonly CustomerDevis $customerDevis
) {
parent::__construct();
$items = [];
foreach ($this->customerDevis->getCustomerDevisLines() as $line) {
$items[$line->getPos()] = [
'title' => $line->getName(),
'content' => $line->getContent(),
'priceHt' => $line->getPriceHT(),
'priceTTC' => (1.20 * $line->getPriceHT()),
'priceTTC' => round(1.20 * $line->getPriceHT(), 2),
];
}
ksort($items);
$this->items = $items;
$this->SetTitle(mb_convert_encoding("Devis N° ", "ISO-8859-1", "UTF-8") . $this->customerDevis->getNumDevis());
$title = mb_convert_encoding("Devis N° " . $this->customerDevis->getNumDevis(), "ISO-8859-1", "UTF-8");
$this->SetTitle($title);
}
function Header()
public function Header(): void
{
$this->SetFont('Arial', '', 10);
$formatter = new IntlDateFormatter(
'fr_FR', // Locale for French (France)
IntlDateFormatter::FULL, // Date style: e.g., jeudi 31 juillet 2025
IntlDateFormatter::NONE, // Time style: none
'Europe/Paris', // Timezone (important for correct date if not UTC)
IntlDateFormatter::GREGORIAN, // Calendar type
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Europe/Paris',
IntlDateFormatter::GREGORIAN
);
$this->Text(15, 80, mb_convert_encoding("DEVIS N° " . $this->customerDevis->getNumDevis(), 'ISO-8859-1', 'UTF-8'));
$this->Text(15, 85, mb_convert_encoding("Saint-Quentin, ".$formatter->format( $this->customerDevis->getCreateAt()->getTimestamp()), 'ISO-8859-1', 'UTF-8'));
$numDevisText = mb_convert_encoding("DEVIS N° " . $this->customerDevis->getNumDevis(), 'ISO-8859-1', 'UTF-8');
$dateText = mb_convert_encoding("Saint-Quentin, " . $formatter->format($this->customerDevis->getCreateAt()), 'ISO-8859-1', 'UTF-8');
$this->Text(15, 80, $numDevisText);
$this->Text(15, 85, $dateText);
$this->SetFont('Arial', 'B', 12);
$y = 60;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($this->customerDevis->getCustomer()->getAddress2() != "") {
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress2(), 'ISO-8859-1', 'UTF-8'));
}
if ($this->customerDevis->getCustomer()->getAddress3() != "") {
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
}
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode() . " " . $this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
$this->body();
$customer = $this->customerDevis->getCustomer();
$this->Text(110, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y += 5;
$this->Text(110, $y, mb_convert_encoding($customer->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($address2 = $customer->getAddress2()) {
$y += 5;
$this->Text(110, $y, mb_convert_encoding($address2, 'ISO-8859-1', 'UTF-8'));
}
if ($address3 = $customer->getAddress3()) {
$y += 5;
$this->Text(110, $y, mb_convert_encoding($address3, 'ISO-8859-1', 'UTF-8'));
}
$y += 5;
$cityLine = $customer->getZipcode() . " " . $customer->getCity();
$this->Text(110, $y, mb_convert_encoding($cityLine, 'ISO-8859-1', 'UTF-8'));
$this->body();
}
public function body()
private function body(): void
{
// Headers for the items table
$this->SetFont('Arial','B',10);
$this->SetXY(145,100);
$this->SetFont('Arial', 'B', 10);
$this->SetXY(145, 100);
$this->Cell(40, 5, mb_convert_encoding("PRIX HT", "ISO-8859-1", "UTF-8"), 0, 0, 'C');
$this->Line(145, 110, 145, 220);
$this->Line(185, 110, 185, 220);
$this->Line(0,100,5,100);
$this->Line(0,200,5,200);
$this->Line(0, 100, 5, 100);
$this->Line(0, 200, 5, 200);
}
/**
* Draws the headers for the items table.
*/
/**
* Generates the main content of the PDF, including the list of items.
* This function has been fixed to correctly display items one after another.
*/
function generate()
public function generate(): void
{
$this->AliasNbPages();
$this->AddPage();
$this->SetFont('Arial', '', 12);
// Set a starting Y position for the items list, just below the item table header.
$startY = 110;
$this->SetY($startY);
// Define a bottom limit for the content to avoid overwriting the summary/footer.
$contentBottomLimit = 220;
foreach ($this->items as $item) {
// A simple check to see if we have enough space for the next item.
// 30 is a rough estimate for item height. For more complex content,
// you might need a more dynamic height calculation.
if ($this->GetY() + 30 > $contentBottomLimit) {
$this->AddPage();
$this->body(); // Redraw the item table header on the new page
$this->SetY($startY); // Reset Y position on the new page
$this->body(); // redraw table headers
$this->SetY($startY);
}
// Store the current Y position to align all columns for this item's title line.
$current_y = $this->GetY();
$currentY = $this->GetY();
// Set position to the start of the line for the main content.
// Title
$this->SetX(20);
$this->SetFont('Arial', 'B', 11);
// Print the title, but don't move to the next line yet (ln=0).
$this->Cell(95, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
// Now, set the position for the prices on the same line using SetXY.
// Prices
$this->SetFont('Arial', '', 11);
$this->SetXY(142, $current_y);
$this->SetXY(142, $currentY);
$this->SetFont('Arial', 'B', 11);
$this->Cell(39, 8, number_format($item['priceHt'], 2, ",") . " " . EURO, 0, 1, 'R');
$this->SetFont('Arial', '', 11);
// The cursor is now on the line below the title/prices.
// Print the item content description.
$this->SetX(30); // Ensure we are in the correct column.
// Content description
$this->SetX(30);
$this->MultiCell(90, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0, 'L');
// Add a small vertical gap between items for readability.
$this->Ln(5);
}
$this->displaySummary();
}
function displaySummary()
private function displaySummary(): void
{
// Calculate totals
$totalHT = array_sum(array_column($this->items, 'priceHt'));
$totalTVA = $totalHT * 0.20;
$totalTTC = $totalHT + $totalTVA;
// Position the summary block at the bottom of the page
$this->SetY(-60);
$this->Cell(30,10,"{{Sign;type=signature;role=First Party}}", 0, 0, 'L');
// Display the summary
$this->Cell(30, 10, "{{Sign;type=signature;role=First Party}}", 0, 0, 'L');
$this->SetFont('Arial', '', 12);
$this->Cell(100, 10, mb_convert_encoding('Total HT :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalHT, 2, ",") . " " . EURO, 0, 1, 'R');
$this->Cell(135, 10, mb_convert_encoding('TVA (20%) :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalTVA, 2, ",") . " " . EURO, 0, 1, 'R');
$this->SetFont('Arial', 'B', 12);
$this->Cell(135, 10, mb_convert_encoding('Total :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalTTC, 2, ",") . " " . EURO, 0, 1, 'R');

View File

@@ -2,161 +2,154 @@
namespace App\Service\Pdf;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerOrder;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\Label\Font\OpenSans;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\Writer\PngWriter;
use Fpdf\Fpdf;
use IntlDateFormatter;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Uid\Uuid;
define('EURO_FACTURE', chr(128));
class FacturePdf extends Fpdf
{
private $items;
private array $items;
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerOrder $customerDevis)
{
public function __construct(
private readonly KernelInterface $kernel,
private readonly CustomerOrder $customerDevis
) {
parent::__construct();
$items = [];
foreach ($this->customerDevis->getCustomerOrderLines() as $line) {
$items[$line->getPo()] = [
'title' => $line->getName(),
'content' => $line->getContent(),
'priceHt' => $line->getPriceHT(),
'priceTTC' => (1.20 * $line->getPriceHT()),
'priceTTC' => round(1.20 * $line->getPriceHT(), 2),
];
}
ksort($items);
$this->items = $items;
$this->SetTitle(mb_convert_encoding("Facture N° ", "ISO-8859-1", "UTF-8") . $this->customerDevis->getNumOrder());
$title = mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), "ISO-8859-1", "UTF-8");
$this->SetTitle($title);
}
function Header()
public function Header(): void
{
$this->SetFont('Arial', '', 10);
$formatter = new IntlDateFormatter(
'fr_FR', // Locale for French (France)
IntlDateFormatter::FULL, // Date style: e.g., jeudi 31 juillet 2025
IntlDateFormatter::NONE, // Time style: none
'Europe/Paris', // Timezone (important for correct date if not UTC)
IntlDateFormatter::GREGORIAN, // Calendar type
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Europe/Paris',
IntlDateFormatter::GREGORIAN
);
$factureNumber = mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), 'ISO-8859-1', 'UTF-8');
$dateFacture = mb_convert_encoding("Saint-Quentin, " . $formatter->format($this->customerDevis->getCreateAt()), 'ISO-8859-1', 'UTF-8');
$this->Text(15, 80, mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), 'ISO-8859-1', 'UTF-8'));
$this->Text(15, 85, mb_convert_encoding("Saint-Quentin, ".$formatter->format( $this->customerDevis->getCreateAt()->getTimestamp()), 'ISO-8859-1', 'UTF-8'));
$this->Text(15, 80, $factureNumber);
$this->Text(15, 85, $dateFacture);
$this->SetFont('Arial', 'B', 12);
$y = 60;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($this->customerDevis->getCustomer()->getAddress2() != "") {
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress2(), 'ISO-8859-1', 'UTF-8'));
$customer = $this->customerDevis->getCustomer();
$this->Text(110, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y += 5;
$this->Text(110, $y, mb_convert_encoding($customer->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($address2 = $customer->getAddress2()) {
$y += 5;
$this->Text(110, $y, mb_convert_encoding($address2, 'ISO-8859-1', 'UTF-8'));
}
if ($this->customerDevis->getCustomer()->getAddress3() != "") {
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
if ($address3 = $customer->getAddress3()) {
$y += 5;
$this->Text(110, $y, mb_convert_encoding($address3, 'ISO-8859-1', 'UTF-8'));
}
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode() . " " . $this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
$y += 5;
$this->Text(110, $y, mb_convert_encoding(
$customer->getZipcode() . " " . $customer->getCity(),
'ISO-8859-1',
'UTF-8'
));
$this->SetFont('Arial', '', 12);
$this->body();
}
public function body()
private function body(): void
{
// Headers for the items table
$this->SetFont('Arial','B',10);
$this->SetXY(145,100);
$this->SetFont('Arial', 'B', 10);
$this->SetXY(145, 100);
$this->Cell(40, 5, mb_convert_encoding("PRIX HT", "ISO-8859-1", "UTF-8"), 0, 0, 'C');
$this->Line(145, 110, 145, 220);
$this->Line(185, 110, 185, 220);
$this->Line(0,100,5,100);
$this->Line(0,200,5,200);
$this->Line(0, 100, 5, 100);
$this->Line(0, 200, 5, 200);
}
/**
* Generates the main content of the PDF, including the list of items.
* This function has been fixed to correctly display items one after another.
*/
function generate()
public function generate(): void
{
$this->AliasNbPages();
$this->AddPage();
$this->SetFont('Arial', '', 12);
// Set a starting Y position for the items list, just below the item table header.
$startY = 110;
$this->SetY($startY);
// Define a bottom limit for the content to avoid overwriting the summary/footer.
$contentBottomLimit = 220;
foreach ($this->items as $item) {
// A simple check to see if we have enough space for the next item.
// 30 is a rough estimate for item height. For more complex content,
// you might need a more dynamic height calculation.
if ($this->GetY() + 30 > $contentBottomLimit) {
$this->AddPage();
$this->body(); // Redraw the item table header on the new page
$this->SetY($startY); // Reset Y position on the new page
$this->body(); // redraw headers
$this->SetY($startY);
}
// Store the current Y position to align all columns for this item's title line.
$current_y = $this->GetY();
$currentY = $this->GetY();
// Set position to the start of the line for the main content.
// Title
$this->SetX(20);
$this->SetFont('Arial', 'B', 11);
// Print the title, but don't move to the next line yet (ln=0).
$this->Cell(95, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
// Now, set the position for the prices on the same line using SetXY.
$this->SetFont('Arial', '', 11);
$this->SetXY(142, $current_y);
// Price HT
$this->SetFont('Arial', 'B', 11);
$this->SetXY(142, $currentY);
$this->Cell(39, 8, number_format($item['priceHt'], 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
$this->SetFont('Arial', '', 11);
// The cursor is now on the line below the title/prices.
// Print the item content description.
$this->SetX(30); // Ensure we are in the correct column.
// Content description
$this->SetX(30);
$this->MultiCell(90, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0, 'L');
// Add a small vertical gap between items for readability.
$this->Ln(5);
}
$this->displaySummary();
}
function displaySummary()
private function displaySummary(): void
{
// Calculate totals
$totalHT = array_sum(array_column($this->items, 'priceHt'));
$totalTVA = $totalHT * 0.20;
$totalTTC = $totalHT + $totalTVA;
// Position the summary block at the bottom of the page
$this->SetY(-60);
// Display the summary
$this->SetFont('Arial', '', 12);
$this->Cell(135, 10, mb_convert_encoding('Total HT :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalHT, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
$this->Cell(135, 10, mb_convert_encoding('TVA (20%) :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalTVA, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
$this->SetFont('Arial', 'B', 12);
$this->Cell(135, 10, mb_convert_encoding('Total :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalTTC, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');

View File

@@ -16,30 +16,39 @@ define('EURO_AVIS', chr(128));
class PaymentPdf extends Fpdf
{
private $items;
private array $items;
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerAdvertPayment $customerDevis, private readonly string $urlPaiment)
{
public function __construct(
private readonly KernelInterface $kernel,
private readonly CustomerAdvertPayment $customerDevis,
private readonly string $urlPaiment
) {
parent::__construct();
$items = [];
foreach ($this->customerDevis->getCustomerAdvertPaymentLines() as $line) {
$items[$line->getPos()] = [
'title' => $line->getName(),
'content' => $line->getContent(),
'priceHt' => $line->getPriceHT(),
'priceTTC' => (1.20 * $line->getPriceHT()),
'priceTTC' => round(1.20 * $line->getPriceHT(), 2),
];
}
ksort($items);
$this->items = $items;
$this->SetTitle(mb_convert_encoding("Avis de paiment N° ", "ISO-8859-1", "UTF-8") . $this->customerDevis->getNumAvis());
$title = mb_convert_encoding("Avis de paiement N° " . $this->customerDevis->getNumAvis(), "ISO-8859-1", "UTF-8");
$this->SetTitle($title);
}
function Header()
public function Header(): void
{
$this->Image($this->kernel->getProjectDir() . "/public/assets/logo_siteconseil.png", 5, 5, 25);
$this->SetFont('Arial', 'B', 12);
$this->Text(30, 10, mb_convert_encoding("SITECONSEIL", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 12);
$this->Text(30, 15, mb_convert_encoding("27 rue le sérurier", 'ISO-8859-1', 'UTF-8'));
$this->Text(30, 20, mb_convert_encoding("02100 SAINT-QUENTIN", 'ISO-8859-1', 'UTF-8'));
@@ -48,32 +57,39 @@ class PaymentPdf extends Fpdf
$this->Text(8, 35, mb_convert_encoding("SIRET: 41866405800025", 'ISO-8859-1', 'UTF-8'));
$this->Text(8, 40, mb_convert_encoding("RCS: RCS St-Quentin 418 664 058", 'ISO-8859-1', 'UTF-8'));
$this->Text(8, 45, mb_convert_encoding("TVA: FR05418664058", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 12);
$this->Text(125, 10, mb_convert_encoding("AVIS DE PAIEMENT N° " . $this->customerDevis->getNumAvis(), 'ISO-8859-1', 'UTF-8'));
$this->Text(125, 15, mb_convert_encoding("Date: " . $this->customerDevis->getCreateAt()->format('d/m/Y'), 'ISO-8859-1', 'UTF-8'));
$y = 40;
$this->Text(120, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y = $y + 5;
$this->Text(120, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($this->customerDevis->getCustomer()->getAddress2() != "") {
$y = $y + 5;
$this->Text(120, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress2(), 'ISO-8859-1', 'UTF-8'));
$customer = $this->customerDevis->getCustomer();
$this->Text(120, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y += 5;
$this->Text(120, $y, mb_convert_encoding($customer->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($address2 = $customer->getAddress2()) {
$y += 5;
$this->Text(120, $y, mb_convert_encoding($address2, 'ISO-8859-1', 'UTF-8'));
}
if ($this->customerDevis->getCustomer()->getAddress3() != "") {
$y = $y + 5;
$this->Text(120, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
if ($address3 = $customer->getAddress3()) {
$y += 5;
$this->Text(120, $y, mb_convert_encoding($address3, 'ISO-8859-1', 'UTF-8'));
}
$y = $y + 5;
$this->Text(120, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode() . " " . $this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
$y += 5;
$this->Text(120, $y, mb_convert_encoding($customer->getZipcode() . " " . $customer->getCity(), 'ISO-8859-1', 'UTF-8'));
$this->body();
}
function Footer()
public function Footer(): void
{
$this->SetY(-20);
$this->SetFont('Arial', 'I', 8);
$this->Cell(0, 5, 'SITECONSEIL - 27 rue le serrurier - 02100 SAINT-QUENTIN - s.com@siteconseil.fr', 0, 0, 'C');
$this->Ln(5);
$this->Cell(0, 5, '03 23 05 62 43 - SIRET: 41866405800025 - RCS: RCS St-Quentin - TVA: FR05418664058', 0, 0, 'C');
@@ -81,10 +97,9 @@ class PaymentPdf extends Fpdf
$this->Cell(0, 5, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
public function body()
private function body(): void
{
// Headers for the items table
$this->SetFont('Arial','B',10);
$this->SetFont('Arial', 'B', 10);
$this->SetY(65);
$this->SetX(120);
$this->Cell(40, 5, mb_convert_encoding("PRIX HT", "ISO-8859-1", "UTF-8"), 0, 0, 'R');
@@ -92,65 +107,49 @@ class PaymentPdf extends Fpdf
$this->Line(10, 70, 200, 70);
}
/**
* Generates the main content of the PDF, including the list of items.
* This function has been fixed to correctly display items one after another.
*/
function generate()
public function generate(): void
{
$this->AliasNbPages();
$this->AddPage();
$this->SetFont('Arial', '', 12);
// Set a starting Y position for the items list, just below the item table header.
$startY = 75;
$this->SetY($startY);
// Define a bottom limit for the content to avoid overwriting the summary/footer.
$contentBottomLimit = 220;
foreach ($this->items as $item) {
// A simple check to see if we have enough space for the next item.
// 30 is a rough estimate for item height. For more complex content,
// you might need a more dynamic height calculation.
if ($this->GetY() + 30 > $contentBottomLimit) {
$this->AddPage();
$this->body(); // Redraw the item table header on the new page
$this->SetY($startY); // Reset Y position on the new page
$this->body();
$this->SetY($startY);
}
// Store the current Y position to align all columns for this item's title line.
$current_y = $this->GetY();
$currentY = $this->GetY();
// Set position to the start of the line for the main content.
// Title
$this->SetX(10);
$this->SetFont('Arial', 'B', 12);
// Print the title, but don't move to the next line yet (ln=0).
$this->Cell(110, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
// Now, set the position for the prices on the same line using SetXY.
// Prices HT and TTC
$this->SetFont('Arial', '', 12);
$this->SetXY(120, $current_y);
$this->SetXY(120, $currentY);
$this->Cell(40, 10, number_format($item['priceHt'], 2, ",") . " " . EURO_AVIS, 0, 0, 'R');
$this->SetXY(160, $current_y);
// The last cell on the line moves the cursor to the next line (ln=1).
$this->SetXY(160, $currentY);
$this->Cell(40, 10, number_format($item['priceTTC'], 2, ",") . " " . EURO_AVIS, 0, 1, 'R');
// The cursor is now on the line below the title/prices.
// Print the item content description.
$this->SetX(10); // Ensure we are in the correct column.
// Content description
$this->SetX(10);
$this->MultiCell(110, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0, 'L');
// Add a small vertical gap between items for readability.
$this->Ln(5);
}
$this->displaySummary();
}
function displaySummary()
private function displaySummary(): void
{
$builder = new Builder(
writer: new PngWriter(),
@@ -163,6 +162,7 @@ class PaymentPdf extends Fpdf
labelFont: new OpenSans(30),
labelAlignment: LabelAlignment::Center
);
$result = $builder->build();
$tmpname = Uuid::v4() . ".png";
@@ -170,17 +170,14 @@ class PaymentPdf extends Fpdf
$result->saveToFile($dir);
$this->Image($dir, 25, 235, 35, 35);
@unlink($dir); // Clean up the temp file
@unlink($dir);
// Calculate totals
$totalHT = array_sum(array_column($this->items, 'priceHt'));
$totalTVA = $totalHT * 0.20;
$totalTTC = $totalHT + $totalTVA;
// Position the summary block at the bottom of the page
$this->SetY(-60);
// Display the summary
$this->SetFont('Arial', 'B', 12);
$this->Cell(150, 10, mb_convert_encoding('Total HT:', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalHT, 2, ",") . " " . EURO_AVIS, 0, 1, 'R');

View File

@@ -1,24 +1,15 @@
<?php
namespace App\Service\ResetPassword\Event;
namespace App\Service\ResetPassword\Event;
class ResetPasswordConfirmEvent
{
private string $password;
public function __construct(private string $password)
{
}
/**
* @return string
*/
public function getPassword(): string
{
return $this->password;
}
/**
* @param string $password
*/
public function setPassword(string $password): void
{
$this->password = $password;
}
}

View File

@@ -1,22 +1,13 @@
<?php
namespace App\Service\ResetPassword\Event;
namespace App\Service\ResetPassword\Event;
class ResetPasswordEvent
{
private string $email;
/**
* @param string $email
*/
public function setEmail(string $email): void
public function __construct(private string $email)
{
$this->email = $email;
}
/**
* @return string
*/
public function getEmail(): string
{
return $this->email;

View File

@@ -1,57 +0,0 @@
<?php
namespace App\Service\ResetPassword;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Service\Generator\TempPasswordGenerator;
use App\Service\Mailer\Event\CreatedAdminEvent;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsEventListener(event: ResetPasswordEvent::class, method: 'onResetPassword')]
class ResetPasswordPubscriber
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator,private readonly EntityManagerInterface $entityManager,private readonly Mailer $mailer)
{
}
public function onResetPassword(ResetPasswordEvent $resetPasswordEvent)
{
$email = $resetPasswordEvent->getEmail();
$account = $this->entityManager->getRepository(Account::class)->findOneBy(['email'=>$email]);
if($account instanceof Account) {
$checkExit = $this->entityManager->getRepository(AccountResetPasswordRequest::class)->findOneBy(['Account'=>$account]);
$sendNewRequest = true;
$t = new \DateTimeImmutable();
$request = null;
if($checkExit instanceof AccountResetPasswordRequest) {
$expiredAt = $checkExit->getExpiresAt();
if ($expiredAt < $t) {
$this->entityManager->remove($checkExit);
} else {
$sendNewRequest = false;
$request = $checkExit;
}
}
if($sendNewRequest) {
$expiredAt = $t->modify("+1 hours");
$request = new AccountResetPasswordRequest();
$request->setAccount($account);
$request->setToken(TempPasswordGenerator::generate(50, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"));
$request->setRequestedAt($t);
$request->setExpiresAt($expiredAt);
$this->entityManager->persist($request);
$this->entityManager->flush();
}
$this->mailer->send($account->getEmail(),$account->getUsername(),"[Mainframe] - Lien pour réinitialiser votre mot de passe","mails/artemis/reset.twig",[
'account'=>$account,
'request'=>$request,
'resetLink' => $this->urlGenerator->generate('app_forgotpassword_confirm',['id'=>$account->getId(),'token'=>$request->getToken()],UrlGeneratorInterface::ABSOLUTE_URL),
]);
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Service\ResetPassword;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Service\Generator\TempPasswordGenerator;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsEventListener(event: ResetPasswordEvent::class, method: 'onResetPassword')]
class ResetPasswordSubscriber
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly EntityManagerInterface $entityManager,
private readonly Mailer $mailer
) {
}
public function onResetPassword(ResetPasswordEvent $resetPasswordEvent): void
{
$email = $resetPasswordEvent->getEmail();
$account = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => $email]);
if (!$account instanceof Account) {
return;
}
$existingRequest = $this->entityManager->getRepository(AccountResetPasswordRequest::class)
->findOneBy(['account' => $account]);
$now = new \DateTimeImmutable();
$sendNewRequest = true;
$request = null;
if ($existingRequest instanceof AccountResetPasswordRequest) {
$expiredAt = $existingRequest->getExpiresAt();
if ($expiredAt < $now) {
$this->entityManager->remove($existingRequest);
} else {
$sendNewRequest = false;
$request = $existingRequest;
}
}
if ($sendNewRequest) {
$expiredAt = $now->modify('+1 hour');
$request = new AccountResetPasswordRequest();
$request->setAccount($account);
$request->setToken(TempPasswordGenerator::generate(50, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'));
$request->setRequestedAt($now);
$request->setExpiresAt($expiredAt);
$this->entityManager->persist($request);
$this->entityManager->flush();
}
$resetLink = $this->urlGenerator->generate(
'app_forgotpassword_confirm',
['id' => $account->getId(), 'token' => $request->getToken()],
UrlGeneratorInterface::ABSOLUTE_URL
);
$this->mailer->send(
$account->getEmail(),
$account->getUsername(),
'[Mainframe] - Lien pour réinitialiser votre mot de passe',
'mails/artemis/reset.twig',
[
'account' => $account,
'request' => $request,
'resetLink' => $resetLink,
]
);
}
}

View File

@@ -10,48 +10,74 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class RevendeurService
{
private const ZONE_ID = '7b8ae9e2a488d574b19dfa72a67326d9';
private const DNS_SUFFIX = '-demande.esy-web.fr';
private const DNS_CONTENT_IP = '35.204.191.160';
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly RevendeurRepository $revendeurRepository,
private readonly HttpClientInterface $httpClient
)
{
private readonly RevendeurRepository $revendeurRepository,
private readonly HttpClientInterface $httpClient
) {
}
/**
* Retourne la liste des revendeurs avec le champ dns ajouté.
*
* @return Revendeur[]
*/
public function list(): array
{
$lists = [];
foreach ($this->revendeurRepository->findAll() as $list) {
$list->dns = $list->getCode() . "-demande.esy-web.fr";
$lists[] = $list;
foreach ($this->revendeurRepository->findAll() as $revendeur) {
// Ajout dynamique du champ dns
$revendeur->dns = $revendeur->getCode() . self::DNS_SUFFIX;
$lists[] = $revendeur;
}
return $lists;
}
public function create(\App\Entity\Revendeur $r)
/**
* Crée un revendeur en base et ajoute un enregistrement DNS Cloudflare.
*
* @param Revendeur $revendeur
* @throws TransportExceptionInterface en cas derreur HTTP
*/
public function create(Revendeur $revendeur): void
{
$this->entityManager->persist($r);
$this->entityManager->persist($revendeur);
$this->entityManager->flush();
$dns = $r->getCode() . "-demande.esy-web.fr";
$ZONE_ID = "7b8ae9e2a488d574b19dfa72a67326d9";
$this->httpClient->request('POST', 'https://api.cloudflare.com/client/v4/zones/' . $ZONE_ID . '/dns_records', [
"headers" => [
$dns = $revendeur->getCode() . self::DNS_SUFFIX;
$token = $_ENV['CLOUDFLARE_TOKEN'] ?? '';
$this->httpClient->request('POST', sprintf(
'https://api.cloudflare.com/client/v4/zones/%s/dns_records',
self::ZONE_ID
), [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $_ENV['CLOUDFLARE_TOKEN'],
'Authorization' => 'Bearer ' . $token,
],
'json' => [
'name' => $dns,
'ttl' => 1,
'type' => 'A',
'comment' => 'Link for ' . $r->getRaisonSocial() . " for created",
'content' => '35.204.191.160',
'comment' => 'Link for ' . $revendeur->getRaisonSocial() . ' for created',
'content' => self::DNS_CONTENT_IP,
'proxied' => true,
]
],
]);
}
public function get($id) : Revendeur
/**
* Récupère un revendeur par son ID.
*
* @param int $id
* @return Revendeur|null
*/
public function get(int $id): ?Revendeur
{
return $this->revendeurRepository->find($id);
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Service\Revendeur;
use App\Service\Mailer\Mailer;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsEventListener(event: SendLinkEvent::class, method: 'onSendLinkEvent')]
class RevendeurSubcriber
{
public function __construct(private readonly Mailer $mailer)
{
}
public function onSendLinkEvent(SendLinkEvent $sendLinkEvent)
{
$revendeur = $sendLinkEvent->getRevendeur();
$this->mailer->send($revendeur->getEmail(),$revendeur->getRaisonSocial(),"[ESY-WEB] - Lien pour vos demande d'ouverture du site","mails/revendeur/link.twig",[
"url" => $revendeur->getCode()."-demande.esy-web.fr",
'revendeur' => $revendeur
]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Service\Revendeur;
use App\Service\Mailer\Mailer;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsEventListener(event: SendLinkEvent::class, method: 'onSendLinkEvent')]
class RevendeurSubscriber
{
public function __construct(private readonly Mailer $mailer)
{
}
public function onSendLinkEvent(SendLinkEvent $event): void
{
$revendeur = $event->getRevendeur();
$subject = '[ESY-WEB] - Lien pour vos demandes d\'ouverture du site';
$template = 'mails/revendeur/link.twig';
$context = [
'url' => $revendeur->getCode() . '-demande.esy-web.fr',
'revendeur' => $revendeur,
];
$this->mailer->send($revendeur->getEmail(), $revendeur->getRaisonSocial(), $subject, $template, $context);
}
}

View File

@@ -6,19 +6,12 @@ use App\Entity\Revendeur;
class SendLinkEvent
{
private Revendeur $revendeur;
public function __construct(Revendeur $revendeur)
public function __construct(private readonly Revendeur $revendeur)
{
$this->revendeur = $revendeur;
}
/**
* @return Revendeur
*/
public function getRevendeur(): Revendeur
{
return $this->revendeur;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Service\Stancer;
use App\Entity\Customer as AppCustomer;
use Doctrine\ORM\EntityManagerInterface;
use Stancer\Config;
use Stancer\Customer;
@@ -12,23 +13,40 @@ class Client
public function __construct(private readonly EntityManagerInterface $entityManager)
{
$this->client = Config::init([$_ENV['STANCER_PUBLIC_KEY'], $_ENV['STANCER_PRIVATE_KEY']]);
$this->client->setMode($_ENV['STANCER_ENV']);
$publicKey = $_ENV['STANCER_PUBLIC_KEY'] ?? '';
$privateKey = $_ENV['STANCER_PRIVATE_KEY'] ?? '';
$env = $_ENV['STANCER_ENV'] ?? 'test';
$this->client = Config::init([$publicKey, $privateKey]);
$this->client->setMode($env);
}
public function customer(?\App\Entity\Customer $customer)
/**
* Récupère ou crée un client Stancer à partir d'un customer interne.
*
* @param AppCustomer|null $customer
* @return string|array|null Identifiant Stancer ou tableau décodé du résultat de création, ou null si absence de customer
*/
public function customer(?AppCustomer $customer): string|array|null
{
if ($customer === null) {
return null;
}
if($customer->getStancerId() == null) {
$stancerId = $customer->getStancerId();
if ($stancerId === null) {
$customerItem = new Customer();
$customerItem->setEmail($customer->mainContact()->getEmail());
$mainContact = $customer->mainContact();
$customerItem->setEmail($mainContact->getEmail());
$customerItem->setName($customer->getRaisonSocial());
$result = $customerItem->send();
return json_decode($result, true);
}
return $customer->getStancerId();
return $stancerId;
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Service\Vault;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class VaultClient
{
@@ -12,13 +12,13 @@ class VaultClient
private const KEYS = [
'mainframe_logger',
'mainframe_customer'
'mainframe_customer',
];
public function __construct(private readonly HttpClientInterface $httpClient)
{
$this->vaultAddr = rtrim($_ENV['VAULT_ADDR'], '/');
$this->vaultToken = $_ENV['VAULT_TOKEN'];
$this->vaultAddr = rtrim($_ENV['VAULT_ADDR'] ?? '', '/');
$this->vaultToken = $_ENV['VAULT_TOKEN'] ?? '';
$this->ensureTransitEnabled();
$this->ensureKeysExist();
@@ -39,8 +39,9 @@ class VaultClient
]);
$data = $response->toArray(false);
return $data['data']['ciphertext'] ?? null;
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
return null;
}
}
@@ -60,8 +61,13 @@ class VaultClient
]);
$data = $response->toArray(false);
return isset($data['data']['plaintext']) ? base64_decode($data['data']['plaintext']) : null;
} catch (TransportExceptionInterface $e) {
if (isset($data['data']['plaintext'])) {
return base64_decode($data['data']['plaintext']);
}
return null;
} catch (TransportExceptionInterface) {
return null;
}
}
@@ -81,6 +87,7 @@ class VaultClient
if (!isset($data['transit/'])) {
$enableUrl = sprintf('%s/v1/sys/mounts/transit', $this->vaultAddr);
$this->httpClient->request('POST', $enableUrl, [
'headers' => [
'X-Vault-Token' => $this->vaultToken,
@@ -92,7 +99,7 @@ class VaultClient
}
return true;
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
return false;
}
}
@@ -114,7 +121,8 @@ class VaultClient
'X-Vault-Token' => $this->vaultToken,
],
]);
} catch (\Exception $exception) {
} catch (TransportExceptionInterface) {
// Si la clé n'existe pas, la créer
$this->httpClient->request('POST', $url, [
'headers' => [
'X-Vault-Token' => $this->vaultToken,
@@ -136,8 +144,9 @@ class VaultClient
'X-Vault-Token' => $this->vaultToken,
],
]);
return true;
} catch (TransportExceptionInterface $e) {
} catch (TransportExceptionInterface) {
return false;
}
}

12
total.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
WEBHOOK_URL="https://discord.com/api/webhooks/1421437443688890500/DPSuS00Ian6O0lQw-1aDQPkVB19so4AA5zaLN_nALs3fDGah1KSLBWys_CYpsc33PGIG"
TODAY=$(date +"%Y-%m-%d")
COMMITS_COUNT=$(git log --since="$TODAY 00:00" --until="$TODAY 23:59" --oneline | wc -l)
MESSAGE="Date : $TODAY
Nombre de commits aujourd'hui : $COMMITS_COUNT"
JSON_PAYLOAD=$(jq -n --arg content "$MESSAGE" '{content: $content}')
curl -H "Content-Type: application/json" -d "$JSON_PAYLOAD" "$WEBHOOK_URL"