fix: corrections SonarQube - qualité code, accessibilité, complexité cognitive

Propriétés inutilisées supprimées :
- CheckDnsCommand : suppression de $urlGenerator (jamais lu, seulement injecté)
- PurgeEmailTrackingCommand : suppression de $repository (jamais lu, requêtes
  via $em->createQueryBuilder directement), suppression import EmailTrackingRepository

Corrections PHPStan / types :
- SyncController : suppression $wh['status'] ?? 'created' redondant, accès direct
  à $wh['status'] car le type retour inclut désormais status: string
- StripeWebhookService : PHPDoc createAllWebhooks corrigé de
  list<array{type, url, id}> vers list<array{type, url, id, status, secret?}>
  pour refléter les clés status et secret effectivement présentes
- DnsReportController : suppression ?? '' sur EXPECTED_MX[$domain] (clé toujours existante)
- CloudflareService : ajout @param array<string, mixed> $query sur request()
- CheckDnsCommand : suppression ?? '' sur EXPECTED_MX[$domain], ajout PHPDoc
  @param list<array<string, mixed>> $cfRecords sur checkMailcow

Méthode manquante :
- DnsCheckService : ajout getDkimTxtRecord() qui parcourt les TXT records
  et retourne le premier commençant par 'v=DKIM1', appelé dans checkDkim()

Code mort supprimé :
- MailcowService : suppression is_array($data) toujours vrai sur retour
  de $response->toArray(false), retour direct
- DnsInfraHelper : suppression getFirstTxtValueRaw() identique à getFirstTxtValue(),
  simplification de getActualDnsValue() qui n'appelle plus le fallback

Constantes pour littéraux dupliqués :
- DnsInfraHelper : ajout LABEL_AWS_SES, LABEL_MAILCOW, LABEL_MAILCOW_DNS,
  NOT_FOUND, NOT_CONFIGURED — remplace les chaînes 'AWS SES' (10×),
  'Non trouve' (4×), 'Non configure' (3×), 'Mailcow' et 'Mailcow DNS'
- Utilisation dans CheckDnsCommand (checkAwsSes, checkSesDomain, checkSesDkim,
  checkSesMailFrom, checkSesBounce, checkMailcow)

Réduction complexité cognitive checkAwsSes (61 → ~10 par méthode) :
- Extraction checkSesDomain() : vérifie isDomainVerified, ajoute check + erreur/succès
- Extraction checkSesDkim() : vérifie getDkimStatus (enabled+verified),
  parcourt les tokens DKIM CNAME avec enrichLastCheck
- Extraction checkSesMailFrom() : vérifie getMailFromStatus, MAIL FROM MX
  (checkMxExists + getMxValues), MAIL FROM TXT (checkTxtContains + getTxtSpfValue)
- Extraction checkSesBounce() : vérifie getNotificationStatus (forwarding ou bounce_topic)

Accessibilité WCAG AA :
- app.scss : contraste sidebar-nav-item augmenté de rgba(255,255,255,0.6)
  à rgba(255,255,255,0.75) pour ratio de contraste suffisant sur fond sombre
- tarification/index.html.twig : ajout for/id sur les 5 paires label/input
  (title-{id}, priceHt-{id}, monthPrice-{id}, period-{id}, description-{id})
- membres.html.twig : ajout for/id sur les 15 checkboxes de groupes
  (group-member, group-admin, group-esy-web, ..., group-esy-ndd),
  remplacement du label titre par <span> (n'est pas associé à un contrôle)
- 2fa_google.html.twig : ajout for="trusted-device" et id="trusted-device"
  sur le checkbox de confiance appareil
- tarif.html.twig : ajout <thead class="sr-only"> avec <th>Option</th><th>Tarif</th>
  sur la table options Esy-Mail (table sans en-têtes)

Ansible :
- vault.yml : ajout discord_webhook pour le déploiement prod

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-03 09:41:17 +02:00
parent 911a92ce88
commit 389b2c308c
15 changed files with 183 additions and 208 deletions

View File

@@ -19,6 +19,7 @@ cloudflare_key: cfut_xqEEvg5LDezheCI9rWsd4JdfflvLH5vjmeMp7QHO442dd83b
mailcow_api_key: DF0E7E-0FD059-16226F-8ECFF1-E558B3 mailcow_api_key: DF0E7E-0FD059-16226F-8ECFF1-E558B3
docuseal_api: pgAU116mCFmeF7WQSezHqxtZW8V1fgo31u5d2FXoaKe docuseal_api: pgAU116mCFmeF7WQSezHqxtZW8V1fgo31u5d2FXoaKe
docuseal_webhooks_secret: CRM_COSLAY docuseal_webhooks_secret: CRM_COSLAY
discord_webhook: https://discord.com/api/webhooks/1419573620602044518/ikAdxWxsrrTqMTb5Gh_8ylcoJHlOnq7aJZvR5udoS_fCK56Jk3qpEnJHVKdD8fwuNJF3
smime_private_key: | smime_private_key: |
Bag Attributes Bag Attributes
localKeyID: 75 15 E3 C2 1D 7B 61 75 99 B9 22 D8 FD A4 19 AC 6B BE 1F 8F localKeyID: 75 15 E3 C2 1D 7B 61 75 99 B9 22 D8 FD A4 19 AC 6B BE 1F 8F

View File

@@ -228,7 +228,7 @@ body.glass-bg {
letter-spacing: 0.08em; letter-spacing: 0.08em;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
transition: all 0.2s ease; transition: all 0.2s ease;
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.75);
&:hover { &:hover {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);

View File

@@ -13,7 +13,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
use Twig\Environment; use Twig\Environment;
@@ -32,7 +31,6 @@ class CheckDnsCommand extends Command
private MailerService $mailer, private MailerService $mailer,
private Environment $twig, private Environment $twig,
private HttpClientInterface $httpClient, private HttpClientInterface $httpClient,
private UrlGeneratorInterface $urlGenerator,
private DnsInfraHelper $helper, private DnsInfraHelper $helper,
#[Autowire('%kernel.environment%')] private string $appEnv, #[Autowire('%kernel.environment%')] private string $appEnv,
#[Autowire(env: 'DISCORD_WEBHOOK')] private string $discordWebhook = '', #[Autowire(env: 'DISCORD_WEBHOOK')] private string $discordWebhook = '',
@@ -63,7 +61,7 @@ class CheckDnsCommand extends Command
$this->dnsCheck->checkDmarc($domain, $checks, $errors, $successes); $this->dnsCheck->checkDmarc($domain, $checks, $errors, $successes);
$this->helper->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords); $this->helper->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords);
$this->dnsCheck->checkMx($domain, DnsInfraHelper::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes); $this->dnsCheck->checkMx($domain, DnsInfraHelper::EXPECTED_MX[$domain], $checks, $errors, $successes);
$this->helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords); $this->helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
$this->dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes); $this->dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes);
@@ -128,142 +126,121 @@ class CheckDnsCommand extends Command
*/ */
private function checkAwsSes(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords = []): void private function checkAwsSes(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords = []): void
{ {
$ses = DnsInfraHelper::LABEL_AWS_SES;
if (!$this->awsSes->isAvailable()) { if (!$this->awsSes->isAvailable()) {
$checks[] = DnsCheckService::check('AWS SES', 'API', 'warning', 'Cles non configurees', 'Acces API SES', 'N/A'); $checks[] = DnsCheckService::check($ses, 'API', 'warning', 'Cles non configurees', 'Acces API SES', 'N/A');
return; return;
} }
try { try {
$verif = $this->awsSes->isDomainVerified($domain); $this->checkSesDomain($domain, $checks, $errors, $successes);
$checks[] = DnsCheckService::check( $this->checkSesDkim($domain, $checks, $errors, $successes, $cfRecords);
'AWS SES', 'Domaine', 'Success' === $verif ? 'ok' : 'error', $this->checkSesMailFrom($domain, $checks, $errors, $successes, $cfRecords);
$verif ?? 'Non verifie', 'Success', $verif ?? 'Absent' $this->checkSesBounce($domain, $checks);
);
if ('Success' === $verif) {
$successes[] = "[$domain] AWS SES : domaine verifie";
} else {
$errors[] = "[$domain] AWS SES : domaine non verifie ($verif)";
}
$dkim = $this->awsSes->getDkimStatus($domain);
$dkimOk = $dkim['enabled'] && $dkim['verified'];
$checks[] = DnsCheckService::check(
'AWS SES', 'DKIM statut', $dkimOk ? 'ok' : 'error',
$dkimOk ? 'Active et verifiee' : 'Non active ou non verifiee',
'Enabled=oui, Verified=oui',
'Enabled='.($dkim['enabled'] ? 'oui' : 'non').', Verified='.($dkim['verified'] ? 'oui' : 'non')
);
if ($dkimOk) {
$successes[] = "[$domain] AWS SES DKIM : active et verifiee";
} else {
$errors[] = "[$domain] AWS SES DKIM : non active ou non verifiee";
}
foreach ($dkim['tokens'] as $token) {
$expectedCname = $token.'.dkim.amazonses.com';
$dkimFqdn = $token.'._domainkey.'.$domain;
$actualCname = $this->dnsCheck->getCnameRecord($dkimFqdn);
$found = null !== $actualCname && str_contains($actualCname, 'dkim.amazonses.com');
$checks[] = DnsCheckService::check(
'AWS SES', 'DKIM CNAME '.$token, $found ? 'ok' : 'error',
$found ? 'Present' : 'Absent',
$expectedCname,
$actualCname ?? 'Non trouve'
);
$this->helper->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords);
if ($found) {
$successes[] = "[$domain] AWS SES DKIM CNAME $token : OK";
} else {
$errors[] = "[$domain] AWS SES DKIM CNAME $token : absent (attendu: $dkimFqdn CNAME $expectedCname)";
}
}
$mailFrom = $this->awsSes->getMailFromStatus($domain);
$mailFromDomain = $mailFrom['mail_from_domain'];
if (null !== $mailFromDomain) {
$mailFromStatus = $mailFrom['mail_from_status'];
$checks[] = DnsCheckService::check(
'AWS SES', 'MAIL FROM', 'Success' === $mailFromStatus ? 'ok' : 'error',
$mailFromStatus ?? 'Inconnu',
$mailFromDomain.' (statut: Success)',
$mailFromDomain.' (statut: '.($mailFromStatus ?? '?').')'
);
if ('Success' === $mailFromStatus) {
$successes[] = "[$domain] AWS SES MAIL FROM : $mailFromDomain verifie";
} else {
$errors[] = "[$domain] AWS SES MAIL FROM : $mailFromDomain statut $mailFromStatus";
}
$mxExpected = $mailFrom['mx_expected'];
if (null !== $mxExpected) {
$mxFound = $this->helper->checkMxExists($mailFromDomain, $mxExpected);
$actualMx = $this->helper->getMxValues($mailFromDomain);
$checks[] = DnsCheckService::check(
'AWS SES', 'MAIL FROM MX', $mxFound ? 'ok' : 'error',
$mxFound ? 'Present' : 'Absent',
"$mailFromDomain MX $mxExpected",
$actualMx ?: 'Non trouve'
);
$this->helper->enrichLastCheck($checks, $mailFromDomain, 'MX', $cfRecords);
if ($mxFound) {
$successes[] = "[$domain] AWS SES MAIL FROM MX : OK";
} else {
$errors[] = "[$domain] AWS SES MAIL FROM MX absent (attendu: $mailFromDomain MX $mxExpected)";
}
}
$txtExpected = $mailFrom['txt_expected'];
if (null !== $txtExpected) {
$txtFound = $this->helper->checkTxtContains($mailFromDomain, 'v=spf1');
$actualTxt = $this->helper->getTxtSpfValue($mailFromDomain);
$checks[] = DnsCheckService::check(
'AWS SES', 'MAIL FROM TXT', $txtFound ? 'ok' : 'error',
$txtFound ? 'Present' : 'Absent',
"$mailFromDomain TXT $txtExpected",
$actualTxt ?: 'Non trouve'
);
$this->helper->enrichLastCheck($checks, $mailFromDomain, 'TXT', $cfRecords);
if ($txtFound) {
$successes[] = "[$domain] AWS SES MAIL FROM SPF : OK";
} else {
$errors[] = "[$domain] AWS SES MAIL FROM SPF absent (attendu: $mailFromDomain TXT $txtExpected)";
}
}
} else {
$checks[] = DnsCheckService::check('AWS SES', 'MAIL FROM', 'warning', 'Non configure', 'bounce.'.$domain, 'N/A');
}
$notif = $this->awsSes->getNotificationStatus($domain);
$bounceOk = $notif['forwarding'] || null !== $notif['bounce_topic'];
$bounceDetail = $notif['forwarding'] ? 'Forwarding actif' : ($notif['bounce_topic'] ?? 'Non configure');
$checks[] = DnsCheckService::check('AWS SES', 'Bounce notif', $bounceOk ? 'ok' : 'warning', $bounceDetail, 'Forwarding ou SNS topic', $bounceDetail);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$errors[] = "[$domain] AWS SES : ".$e->getMessage(); $errors[] = "[$domain] $ses : ".$e->getMessage();
$checks[] = DnsCheckService::check('AWS SES', 'API', 'error', $e->getMessage()); $checks[] = DnsCheckService::check($ses, 'API', 'error', $e->getMessage());
} }
} }
/** @param list<array> $checks @param list<string> $errors @param list<string> $successes */
private function checkSesDomain(string $domain, array &$checks, array &$errors, array &$successes): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
$verif = $this->awsSes->isDomainVerified($domain);
$ok = 'Success' === $verif;
$checks[] = DnsCheckService::check($ses, 'Domaine', $ok ? 'ok' : 'error', $verif ?? 'Non verifie', 'Success', $verif ?? 'Absent');
$ok ? $successes[] = "[$domain] $ses : domaine verifie" : $errors[] = "[$domain] $ses : domaine non verifie ($verif)";
}
/** @param list<array> $checks @param list<string> $errors @param list<string> $successes @param list<array<string, mixed>> $cfRecords */
private function checkSesDkim(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
$dkim = $this->awsSes->getDkimStatus($domain);
$dkimOk = $dkim['enabled'] && $dkim['verified'];
$checks[] = DnsCheckService::check(
$ses, 'DKIM statut', $dkimOk ? 'ok' : 'error',
$dkimOk ? 'Active et verifiee' : 'Non active ou non verifiee',
'Enabled=oui, Verified=oui',
'Enabled='.($dkim['enabled'] ? 'oui' : 'non').', Verified='.($dkim['verified'] ? 'oui' : 'non')
);
$dkimOk ? $successes[] = "[$domain] $ses DKIM : active et verifiee" : $errors[] = "[$domain] $ses DKIM : non active ou non verifiee";
foreach ($dkim['tokens'] as $token) {
$expectedCname = $token.'.dkim.amazonses.com';
$dkimFqdn = $token.'._domainkey.'.$domain;
$actualCname = $this->dnsCheck->getCnameRecord($dkimFqdn);
$found = null !== $actualCname && str_contains($actualCname, 'dkim.amazonses.com');
$checks[] = DnsCheckService::check($ses, 'DKIM CNAME '.$token, $found ? 'ok' : 'error', $found ? 'Present' : 'Absent', $expectedCname, $actualCname ?? DnsInfraHelper::NOT_FOUND);
$this->helper->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords);
$found ? $successes[] = "[$domain] $ses DKIM CNAME $token : OK" : $errors[] = "[$domain] $ses DKIM CNAME $token : absent (attendu: $dkimFqdn CNAME $expectedCname)";
}
}
/** @param list<array> $checks @param list<string> $errors @param list<string> $successes @param list<array<string, mixed>> $cfRecords */
private function checkSesMailFrom(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
$mailFrom = $this->awsSes->getMailFromStatus($domain);
$mfd = $mailFrom['mail_from_domain'];
if (null === $mfd) {
$checks[] = DnsCheckService::check($ses, 'MAIL FROM', 'warning', DnsInfraHelper::NOT_CONFIGURED, 'bounce.'.$domain, 'N/A');
return;
}
$status = $mailFrom['mail_from_status'];
$statusOk = 'Success' === $status;
$checks[] = DnsCheckService::check($ses, 'MAIL FROM', $statusOk ? 'ok' : 'error', $status ?? 'Inconnu', $mfd.' (statut: Success)', $mfd.' (statut: '.($status ?? '?').')');
$statusOk ? $successes[] = "[$domain] $ses MAIL FROM : $mfd verifie" : $errors[] = "[$domain] $ses MAIL FROM : $mfd statut $status";
if (null !== $mailFrom['mx_expected']) {
$mxFound = $this->helper->checkMxExists($mfd, $mailFrom['mx_expected']);
$actualMx = $this->helper->getMxValues($mfd);
$checks[] = DnsCheckService::check($ses, 'MAIL FROM MX', $mxFound ? 'ok' : 'error', $mxFound ? 'Present' : 'Absent', "$mfd MX {$mailFrom['mx_expected']}", $actualMx ?: DnsInfraHelper::NOT_FOUND);
$this->helper->enrichLastCheck($checks, $mfd, 'MX', $cfRecords);
$mxFound ? $successes[] = "[$domain] $ses MAIL FROM MX : OK" : $errors[] = "[$domain] $ses MAIL FROM MX absent (attendu: $mfd MX {$mailFrom['mx_expected']})";
}
if (null !== $mailFrom['txt_expected']) {
$txtFound = $this->helper->checkTxtContains($mfd, 'v=spf1');
$actualTxt = $this->helper->getTxtSpfValue($mfd);
$checks[] = DnsCheckService::check($ses, 'MAIL FROM TXT', $txtFound ? 'ok' : 'error', $txtFound ? 'Present' : 'Absent', "$mfd TXT {$mailFrom['txt_expected']}", $actualTxt ?: DnsInfraHelper::NOT_FOUND);
$this->helper->enrichLastCheck($checks, $mfd, 'TXT', $cfRecords);
$txtFound ? $successes[] = "[$domain] $ses MAIL FROM SPF : OK" : $errors[] = "[$domain] $ses MAIL FROM SPF absent (attendu: $mfd TXT {$mailFrom['txt_expected']})";
}
}
/** @param list<array> $checks */
private function checkSesBounce(string $domain, array &$checks): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
$notif = $this->awsSes->getNotificationStatus($domain);
$bounceOk = $notif['forwarding'] || null !== $notif['bounce_topic'];
$detail = $notif['forwarding'] ? 'Forwarding actif' : ($notif['bounce_topic'] ?? DnsInfraHelper::NOT_CONFIGURED);
$checks[] = DnsCheckService::check($ses, 'Bounce notif', $bounceOk ? 'ok' : 'warning', $detail, 'Forwarding ou SNS topic', $detail);
}
/** /**
* @param list<array> $checks * @param list<array> $checks
* @param list<string> $errors * @param list<string> $errors
* @param list<string> $warnings * @param list<string> $warnings
* @param list<string> $successes * @param list<string> $successes
* @param list<array<string, mixed>> $cfRecords
*/ */
private function checkMailcow(string $domain, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords = []): void private function checkMailcow(string $domain, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords = []): void
{ {
$mc = DnsInfraHelper::LABEL_MAILCOW;
if (!$this->mailcow->isAvailable()) { if (!$this->mailcow->isAvailable()) {
$checks[] = DnsCheckService::check('Mailcow', 'API', 'warning', 'Non disponible', 'Acces API Mailcow', 'N/A'); $checks[] = DnsCheckService::check($mc, 'API', 'warning', 'Non disponible', 'Acces API Mailcow', 'N/A');
return; return;
} }
@@ -271,50 +248,44 @@ class CheckDnsCommand extends Command
try { try {
$info = $this->mailcow->getDomainStatus($domain); $info = $this->mailcow->getDomainStatus($domain);
if (null === $info) { if (null === $info) {
$warnings[] = "[$domain] Mailcow : domaine non configure"; $warnings[] = "[$domain] $mc : domaine non configure";
$checks[] = DnsCheckService::check('Mailcow', 'Domaine', 'warning', 'Non configure', 'Domaine actif', 'Absent'); $checks[] = DnsCheckService::check($mc, 'Domaine', 'warning', DnsInfraHelper::NOT_CONFIGURED, 'Domaine actif', 'Absent');
return; return;
} }
$checks[] = DnsCheckService::check( $checks[] = DnsCheckService::check(
'Mailcow', 'Domaine', $info['active'] ? 'ok' : 'error', $mc, 'Domaine', $info['active'] ? 'ok' : 'error',
$info['active'] ? "Actif, {$info['mailboxes']} boite(s)" : 'Desactive', $info['active'] ? "Actif, {$info['mailboxes']} boite(s)" : 'Desactive',
'Actif', $info['active'] ? "Actif ({$info['mailboxes']} boites)" : 'Desactive' 'Actif', $info['active'] ? "Actif ({$info['mailboxes']} boites)" : 'Desactive'
); );
$info['active'] ? $successes[] = "[$domain] $mc : actif, {$info['mailboxes']} boite(s)" : $errors[] = "[$domain] $mc : desactive";
if ($info['active']) { $mcDns = DnsInfraHelper::LABEL_MAILCOW_DNS;
$successes[] = "[$domain] Mailcow : actif, {$info['mailboxes']} boite(s)"; foreach ($this->mailcow->getExpectedDnsRecords($domain) as $expected) {
} else {
$errors[] = "[$domain] Mailcow : desactive";
}
$expectedRecords = $this->mailcow->getExpectedDnsRecords($domain);
foreach ($expectedRecords as $expected) {
$found = $this->helper->checkDnsRecordExists($expected['type'], $expected['name'], $expected['content']); $found = $this->helper->checkDnsRecordExists($expected['type'], $expected['name'], $expected['content']);
$isOptional = $expected['optional']; $isOptional = $expected['optional'];
$label = $expected['type'].' '.$expected['name']; $label = $expected['type'].' '.$expected['name'];
$digValue = $this->helper->getActualDnsValue($expected['type'], $expected['name']); $digValue = $this->helper->getActualDnsValue($expected['type'], $expected['name']);
$checks[] = DnsCheckService::check( $checks[] = DnsCheckService::check(
'Mailcow DNS', $label, $found ? 'ok' : ($isOptional ? 'warning' : 'error'), $mcDns, $label, $found ? 'ok' : ($isOptional ? 'warning' : 'error'),
$found ? 'Present' : ($isOptional ? 'Absent (optionnel)' : 'Absent'), $found ? 'Present' : ($isOptional ? 'Absent (optionnel)' : 'Absent'),
$expected['content'], $digValue ?: 'Non trouve' $expected['content'], $digValue ?: DnsInfraHelper::NOT_FOUND
); );
$this->helper->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords); $this->helper->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords);
if ($found) { if ($found) {
$successes[] = "[$domain] Mailcow DNS : $label OK"; $successes[] = "[$domain] $mcDns : $label OK";
} elseif ($isOptional) { } elseif ($isOptional) {
$warnings[] = "[$domain] Mailcow DNS : $label absent (optionnel)"; $warnings[] = "[$domain] $mcDns : $label absent (optionnel)";
} else { } else {
$errors[] = "[$domain] Mailcow DNS : $label absent"; $errors[] = "[$domain] $mcDns : $label absent";
} }
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$errors[] = "[$domain] Mailcow : ".$e->getMessage(); $errors[] = "[$domain] $mc : ".$e->getMessage();
$checks[] = DnsCheckService::check('Mailcow', 'API', 'error', $e->getMessage()); $checks[] = DnsCheckService::check($mc, 'API', 'error', $e->getMessage());
} }
} }

View File

@@ -2,7 +2,6 @@
namespace App\Command; namespace App\Command;
use App\Repository\EmailTrackingRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@@ -18,7 +17,6 @@ use Symfony\Component\Console\Style\SymfonyStyle;
class PurgeEmailTrackingCommand extends Command class PurgeEmailTrackingCommand extends Command
{ {
public function __construct( public function __construct(
private EmailTrackingRepository $repository,
private EntityManagerInterface $em, private EntityManagerInterface $em,
) { ) {
parent::__construct(); parent::__construct();

View File

@@ -123,8 +123,7 @@ class SyncController extends AbstractController
]; ];
foreach ($result['created'] as $wh) { foreach ($result['created'] as $wh) {
$status = $wh['status'] ?? 'created'; if ('exists' === $wh['status']) {
if ('exists' === $status) {
$this->addFlash('success', $wh['type'].' : deja configure ('.$wh['id'].')'); $this->addFlash('success', $wh['type'].' : deja configure ('.$wh['id'].')');
} else { } else {
$this->addFlash('success', $wh['type'].' : cree ('.$wh['id'].')'); $this->addFlash('success', $wh['type'].' : cree ('.$wh['id'].')');

View File

@@ -54,7 +54,7 @@ class DnsReportController extends AbstractController
$dnsCheck->checkDmarc($domain, $checks, $errors, $successes); $dnsCheck->checkDmarc($domain, $checks, $errors, $successes);
$helper->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords); $helper->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords);
$dnsCheck->checkMx($domain, DnsInfraHelper::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes); $dnsCheck->checkMx($domain, DnsInfraHelper::EXPECTED_MX[$domain], $checks, $errors, $successes);
$helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords); $helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
$dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes); $dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes);

View File

@@ -130,6 +130,7 @@ class CloudflareService
} }
/** /**
* @param array<string, mixed> $query
* @return array<string, mixed> * @return array<string, mixed>
*/ */
private function request(string $method, string $path, array $query = []): array private function request(string $method, string $path, array $query = []): array

View File

@@ -338,6 +338,17 @@ class DnsCheckService
return $records; return $records;
} }
public function getDkimTxtRecord(string $domain): ?string
{
foreach ($this->getTxtRecords($domain) as $txt) {
if (str_starts_with($txt, 'v=DKIM1')) {
return $txt;
}
}
return null;
}
/** /**
* @return list<string> * @return list<string>
*/ */

View File

@@ -11,6 +11,12 @@ class DnsInfraHelper
'esy-web.dev' => 'mail.esy-web.dev', 'esy-web.dev' => 'mail.esy-web.dev',
]; ];
public const LABEL_AWS_SES = 'AWS SES';
public const LABEL_MAILCOW = 'Mailcow';
public const LABEL_MAILCOW_DNS = 'Mailcow DNS';
public const NOT_FOUND = 'Non trouve';
public const NOT_CONFIGURED = 'Non configure';
public function __construct( public function __construct(
private DnsCheckService $dnsCheck, private DnsCheckService $dnsCheck,
private CloudflareService $cloudflare, private CloudflareService $cloudflare,
@@ -58,7 +64,7 @@ class DnsInfraHelper
} }
if (null === $cfValue) { if (null === $cfValue) {
$cfValue = 'Non trouve'; $cfValue = self::NOT_FOUND;
$cfStatus = '' === $cfStatus ? '' : 'error'; $cfStatus = '' === $cfStatus ? '' : 'error';
} }
@@ -76,7 +82,7 @@ class DnsInfraHelper
*/ */
public function enrichLastCheck(array &$checks, string $recordName, string $dnsType, array $cfRecords): void public function enrichLastCheck(array &$checks, string $recordName, string $dnsType, array $cfRecords): void
{ {
$cfValue = 'Non trouve'; $cfValue = self::NOT_FOUND;
$cfStatus = ''; $cfStatus = '';
foreach ($cfRecords as $r) { foreach ($cfRecords as $r) {
@@ -99,7 +105,7 @@ class DnsInfraHelper
return match ($type) { return match ($type) {
'MX' => $this->getMxValues($name), 'MX' => $this->getMxValues($name),
'CNAME' => $this->dnsCheck->getCnameRecord($name) ?? '', 'CNAME' => $this->dnsCheck->getCnameRecord($name) ?? '',
'TXT' => $this->getFirstTxtValue($name) ?: $this->getFirstTxtValueRaw($name), 'TXT' => $this->getFirstTxtValue($name),
'SRV' => $this->getSrvValue($name), 'SRV' => $this->getSrvValue($name),
default => '', default => '',
}; };
@@ -128,19 +134,6 @@ class DnsInfraHelper
return ''; return '';
} }
private function getFirstTxtValueRaw(string $domain): string
{
$output = $this->dnsCheck->dig($domain, 'TXT');
foreach (explode("\n", $output) as $line) {
if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) {
return str_replace('" "', '', $m[1]);
}
}
return '';
}
public function getSrvValue(string $domain): string public function getSrvValue(string $domain): string
{ {
$values = []; $values = [];

View File

@@ -137,8 +137,6 @@ class MailcowService
], ],
]); ]);
$data = $response->toArray(false); return $response->toArray(false);
return \is_array($data) ? $data : [];
} }
} }

View File

@@ -74,7 +74,7 @@ class StripeWebhookService
/** /**
* Cree les 4 webhooks Stripe (main light/instant + connect light/instant). * Cree les 4 webhooks Stripe (main light/instant + connect light/instant).
* *
* @return array{created: list<array{type: string, url: string, id: string}>, errors: list<string>} * @return array{created: list<array{type: string, url: string, id: string, status: string, secret?: string}>, errors: list<string>}
*/ */
public function createAllWebhooks(string $baseUrl): array public function createAllWebhooks(string $baseUrl): array
{ {

View File

@@ -41,66 +41,66 @@
placeholder="email@exemple.fr"> placeholder="email@exemple.fr">
</div> </div>
<div> <div>
<label for="groups-member" class="block text-xs font-bold uppercase tracking-wider mb-2 text-gray-600">Groupes d'acces</label> <span class="block text-xs font-bold uppercase tracking-wider mb-2 text-gray-600">Groupes d'acces</span>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3"> <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all" style="border-color: rgba(99,102,241,0.3);"> <label for="group-member" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all" style="border-color: rgba(99,102,241,0.3);">
<input type="checkbox" id="groups-member" name="groups[]" value="siteconseil_member" class="accent-indigo-600" checked> <input type="checkbox" id="group-member" name="groups[]" value="siteconseil_member" class="accent-indigo-600" checked>
<span class="text-xs font-bold text-indigo-800">Membre</span> <span class="text-xs font-bold text-indigo-800">Membre</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all" style="border-color: rgba(220,38,38,0.3);"> <label for="group-admin" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all" style="border-color: rgba(220,38,38,0.3);">
<input type="checkbox" name="groups[]" value="siteconseil_admin" class="accent-red-600"> <input type="checkbox" id="group-admin" name="groups[]" value="siteconseil_admin" class="accent-red-600">
<span class="text-xs font-bold text-red-800">Super Admin</span> <span class="text-xs font-bold text-red-800">Super Admin</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-web" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-web" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-web" name="groups[]" value="esy-web" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Web</span> <span class="text-xs font-bold">Esy-Web</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-mail" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-mail" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-mail" name="groups[]" value="esy-mail" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Mail</span> <span class="text-xs font-bold">Esy-Mail</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-mailer" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-mailer" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-mailer" name="groups[]" value="esy-mailer" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Mailer</span> <span class="text-xs font-bold">Esy-Mailer</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-analytics" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-analytics" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-analytics" name="groups[]" value="esy-analytics" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Analytics</span> <span class="text-xs font-bold">Esy-Analytics</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-monitor" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-monitor" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-monitor" name="groups[]" value="esy-monitor" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Monitor</span> <span class="text-xs font-bold">Esy-Monitor</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-defender" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-defender" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-defender" name="groups[]" value="esy-defender" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Defender</span> <span class="text-xs font-bold">Esy-Defender</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-translate" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-translate" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-translate" name="groups[]" value="esy-translate" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Translate</span> <span class="text-xs font-bold">Esy-Translate</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-signature" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-signature" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-signature" name="groups[]" value="esy-signature" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Signature</span> <span class="text-xs font-bold">Esy-Signature</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-creator" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-creator" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-creator" name="groups[]" value="esy-creator" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Creator</span> <span class="text-xs font-bold">Esy-Creator</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-aide" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-aide" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-aide" name="groups[]" value="esy-aide" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Aide</span> <span class="text-xs font-bold">Esy-Aide</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-meet" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-meet" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-meet" name="groups[]" value="esy-meet" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Meet</span> <span class="text-xs font-bold">Esy-Meet</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-tchat" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-tchat" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-tchat" name="groups[]" value="esy-tchat" class="accent-[#fabf04]">
<span class="text-xs font-bold">Esy-Tchat</span> <span class="text-xs font-bold">Esy-Tchat</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all"> <label for="group-esy-ndd" class="flex items-center gap-2 px-3 py-2 glass rounded-lg cursor-pointer hover:bg-white/80 transition-all">
<input type="checkbox" name="groups[]" value="esy-ndd" class="accent-[#fabf04]"> <input type="checkbox" id="group-esy-ndd" name="groups[]" value="esy-ndd" class="accent-[#fabf04]">
<span class="text-xs font-bold">Nom de domaine</span> <span class="text-xs font-bold">Nom de domaine</span>
</label> </label>
</div> </div>

View File

@@ -44,20 +44,20 @@
<form method="post" action="{{ path('app_admin_tarification_edit', {id: price.id}) }}" class="p-4"> <form method="post" action="{{ path('app_admin_tarification_edit', {id: price.id}) }}" class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Titre</label> <label for="title-{{ price.id }}" class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Titre</label>
<input type="text" name="title" value="{{ price.title }}" required class="input-glass w-full px-3 py-2 text-sm font-medium"> <input type="text" id="title-{{ price.id }}" name="title" value="{{ price.title }}" required class="input-glass w-full px-3 py-2 text-sm font-medium">
</div> </div>
<div> <div>
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Prix unique HT (&#8364;)</label> <label for="priceHt-{{ price.id }}" class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Prix unique HT (&#8364;)</label>
<input type="text" name="priceHt" value="{{ price.priceHt }}" required class="input-glass w-full px-3 py-2 text-sm font-medium"> <input type="text" id="priceHt-{{ price.id }}" name="priceHt" value="{{ price.priceHt }}" required class="input-glass w-full px-3 py-2 text-sm font-medium">
</div> </div>
<div> <div>
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Prix mensuel HT (&#8364;)</label> <label for="monthPrice-{{ price.id }}" class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Prix mensuel HT (&#8364;)</label>
<input type="text" name="monthPrice" value="{{ price.monthPrice }}" required class="input-glass w-full px-3 py-2 text-sm font-medium"> <input type="text" id="monthPrice-{{ price.id }}" name="monthPrice" value="{{ price.monthPrice }}" required class="input-glass w-full px-3 py-2 text-sm font-medium">
</div> </div>
<div> <div>
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Periode (mois)</label> <label for="period-{{ price.id }}" class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Periode (mois)</label>
<select name="period" class="input-glass w-full px-3 py-2 text-sm font-medium"> <select id="period-{{ price.id }}" name="period" class="input-glass w-full px-3 py-2 text-sm font-medium">
{% for p in [1, 2, 3, 6, 12] %} {% for p in [1, 2, 3, 6, 12] %}
<option value="{{ p }}" {{ price.period == p ? 'selected' }}>{{ p }} mois</option> <option value="{{ p }}" {{ price.period == p ? 'selected' }}>{{ p }} mois</option>
{% endfor %} {% endfor %}
@@ -66,8 +66,8 @@
<input type="hidden" name="stripeId" value="{{ price.stripeId }}"> <input type="hidden" name="stripeId" value="{{ price.stripeId }}">
<input type="hidden" name="stripeAbonnementId" value="{{ price.stripeAbonnementId }}"> <input type="hidden" name="stripeAbonnementId" value="{{ price.stripeAbonnementId }}">
<div class="md:col-span-2 lg:col-span-3"> <div class="md:col-span-2 lg:col-span-3">
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Description</label> <label for="description-{{ price.id }}" class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Description</label>
<textarea name="description" rows="2" class="input-glass w-full px-3 py-2 text-sm font-medium">{{ price.description }}</textarea> <textarea id="description-{{ price.id }}" name="description" rows="2" class="input-glass w-full px-3 py-2 text-sm font-medium">{{ price.description }}</textarea>
</div> </div>
</div> </div>
<div class="mt-4 flex justify-end"> <div class="mt-4 flex justify-end">

View File

@@ -620,6 +620,9 @@
<div class="glass-dark text-white px-4 py-2" style="border-radius: 16px 16px 0 0;"><span class="font-bold uppercase text-xs tracking-widest">Options</span></div> <div class="glass-dark text-white px-4 py-2" style="border-radius: 16px 16px 0 0;"><span class="font-bold uppercase text-xs tracking-widest">Options</span></div>
<div class="p-4"> <div class="p-4">
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead class="sr-only">
<tr><th>Option</th><th>Tarif</th></tr>
</thead>
<tbody> <tbody>
<tr class="border-b border-gray-200"> <tr class="border-b border-gray-200">
<td class="py-2 font-bold">Boite mail supplementaire</td> <td class="py-2 font-bold">Boite mail supplementaire</td>

View File

@@ -37,8 +37,8 @@
</div> </div>
{% if displayTrustedOption %} {% if displayTrustedOption %}
<label class="flex items-center gap-2 cursor-pointer"> <label for="trusted-device" class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="{{ trustedParameterName }}" class="accent-[#fabf04]"> <input type="checkbox" id="trusted-device" name="{{ trustedParameterName }}" class="accent-[#fabf04]">
<span class="text-xs font-bold">Faire confiance a cet appareil pendant 30 jours</span> <span class="text-xs font-bold">Faire confiance a cet appareil pendant 30 jours</span>
</label> </label>
{% endif %} {% endif %}