Commit Graph

65 Commits

Author SHA1 Message Date
Serreau Jovann
cdd5c656a9 feat: ajout signature HMAC SHA-256 sur Devis, Advert et Facture
src/Entity/Devis.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "devis|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals (timing-safe)

src/Entity/Advert.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "advert|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals

src/Entity/Facture.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "facture|numOrder|splitIndex|createdAt"
  signe avec APP_SECRET (inclut splitIndex pour differencier les splits)
- verifyHmac(): verification par hash_equals

src/Service/DevisService.php, AdvertService.php, FactureService.php:
- Injection de APP_SECRET via #[Autowire('%env(APP_SECRET)%')]
- Passage du hmacSecret aux constructeurs des entites

migrations/Version20260402203207.php:
- Ajout colonne hmac VARCHAR(128) NOT NULL sur devis, advert, facture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:32:18 +02:00
Serreau Jovann
5b0e4707f7 feat: ajout des services DevisService, AdvertService et FactureService
src/Service/DevisService.php (nouveau):
- create(): genere un OrderNumber via OrderNumberService::generateAndUse(),
  cree le Devis, persiste et flush. Retourne le Devis.

src/Service/AdvertService.php (nouveau):
- create(?Devis): si un devis est passe, reutilise son OrderNumber
  (meme numero pour devis et advert) et lie l'advert au devis.
  Si pas de devis, genere un nouveau OrderNumber.
- createFromDevis(Devis): raccourci qui appelle create() avec le devis.

src/Service/FactureService.php (nouveau):
- create(?Advert): si un advert est passe, appelle createFromAdvert().
  Si pas d'advert, genere un nouveau OrderNumber (facture seule).
- createFromAdvert(Advert): reutilise le OrderNumber de l'advert
  (meme numero). Gere le splitIndex automatiquement:
  - 1ere facture sur un advert: splitIndex=0 (pas de suffixe)
  - 2eme facture: la 1ere passe a splitIndex=1, la nouvelle a splitIndex=2
    (ex: 04/2026-00001-1 et 04/2026-00001-2)
  - 3eme facture: splitIndex=3 (04/2026-00001-3)

Exemples d'utilisation:
- Facture seule: $factureService->create()
- Devis seul: $devisService->create()
- Advert seul: $advertService->create()
- Devis → Advert: $advertService->createFromDevis($devis)
- Advert → Facture: $factureService->createFromAdvert($advert)
- Advert → 3 Factures: 3x createFromAdvert() → -1, -2, -3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:29:54 +02:00
Serreau Jovann
da7f46f7e9 refactor: renommer Order en Facture + meme OrderNumber partage entre Devis/Advert/Facture
Changement de modele:
- Le meme OrderNumber est partage entre Devis, Advert et Facture
  (ex: 04/2026-00001 pour les 3)
- Les relations OrderNumber passent de OneToOne a ManyToOne pour
  permettre le partage du meme numero

src/Entity/Order.php supprime, remplace par:
src/Entity/Facture.php (nouveau):
- orderNumber: ManyToOne vers OrderNumber (meme numero que l'Advert parent)
- advert: ManyToOne vers Advert (nullable)
- splitIndex: smallint, suffixe pour factures multiples sur un meme advert
  (0 = pas de suffixe, 1 = -1, 2 = -2, etc.)
- getInvoiceNumber(): retourne le numero complet avec suffixe si splitIndex > 0
  (ex: 04/2026-00001 ou 04/2026-00001-2)

src/Entity/Devis.php:
- orderNumber: OneToOne remplace par ManyToOne vers OrderNumber
- adverts: OneToMany vers Advert (inchange)

src/Entity/Advert.php:
- orderNumber: OneToOne remplace par ManyToOne vers OrderNumber
- orders: renomme en factures, OneToMany vers Facture

src/Repository/OrderRepository.php supprime, remplace par:
src/Repository/FactureRepository.php (nouveau)

migrations/Version20260402202809.php:
- Suppression table `order`, creation table facture
- Modification des contraintes unique sur devis et advert
  (unique index supprime car ManyToOne)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:28:30 +02:00
Serreau Jovann
a6e529e643 feat: ajout entity PriceAutomatic pour les tarifs automatiques
src/Entity/PriceAutomatic.php (nouveau):
- id: int auto-increment
- type: string(50), categorie du tarif (ex: esy-web, esy-mail, ndd)
- title: string(255), intitule du tarif
- description: text nullable, detail du tarif
- priceHt: decimal(10,2), prix hors taxes

src/Repository/PriceAutomaticRepository.php (nouveau)

migrations/Version20260402202703.php:
- Table price_automatic avec colonnes type, title, description, price_ht

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:27:12 +02:00
Serreau Jovann
3bda43c72f feat: ajout entities Devis, Advert et Order liees a OrderNumber
Relations:
- Devis → OrderNumber (OneToOne): chaque devis a un numero unique
- Devis → Advert (OneToMany): un devis peut generer plusieurs factures pro forma
- Advert → OrderNumber (OneToOne): chaque facture pro forma a un numero unique
- Advert → Devis (ManyToOne, nullable): un advert peut exister sans devis
- Advert → Order (OneToMany): un advert peut generer plusieurs factures
- Order → OrderNumber (OneToOne): chaque facture a un numero unique
- Order → Advert (ManyToOne, nullable): une facture peut exister sans advert

Chaine: Devis → Advert → Order
Si un advert genere plusieurs factures, le champ splitIndex (smallint)
ajoute un suffixe -X au numero (ex: 04/2026-00001-1, 04/2026-00001-2).
La methode getInvoiceNumber() retourne le numero complet avec suffixe.

src/Entity/Devis.php: id, orderNumber (OneToOne), createdAt, adverts (OneToMany)
src/Entity/Advert.php: id, orderNumber (OneToOne), devis (ManyToOne nullable),
  createdAt, orders (OneToMany)
src/Entity/Order.php: id, orderNumber (OneToOne), advert (ManyToOne nullable),
  splitIndex (smallint default 0), createdAt, getInvoiceNumber()
  Table nommee `order` (mot reserve SQL, echappee avec backticks)

src/Repository/DevisRepository.php, AdvertRepository.php, OrderRepository.php

migrations/Version20260402202554.php: tables devis, advert, `order` avec
  foreign keys vers order_number et relations entre elles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:26:15 +02:00
Serreau Jovann
cacd3ac66c feat: page admin de gestion de la numerotation des commandes
src/Controller/Admin/OrderNumberController.php (nouveau):
- Route /admin/numerotation, accessible ROLE_ROOT uniquement
- index(): affiche le prochain numero via OrderNumberService::preview()
  et les 20 derniers numeros generes (ORDER BY id DESC)
- update(): modifie le prochain numero en creant une entree placeholder
  avec le numero precedent (N-1) marque comme utilise, pour que le
  prochain generate() retourne le numero souhaite
  - Validation du format MM/YYYY-XXXXX via regex
  - Verification que le numero n'existe pas deja
  - Verification que le numero est au minimum 00001

templates/admin/order_number/index.html.twig (nouveau):
- Section "Prochain numero" : affiche le prochain numero en gros (gold)
  avec formulaire pour le modifier (input avec pattern regex,
  placeholder MM/YYYY-XXXXX, explication de l'utilite)
- Section "Derniers numeros generes" : tableau avec numero (font-mono),
  date de creation, statut (Utilise vert / Reserve gris)
- Design glassmorphism (glass, input-glass, btn-gold, glass-dark header)

templates/admin/_layout.html.twig:
- Ajout du lien "Numerotation" dans la sidebar Super Admin avec
  icone hash (#), route app_admin_order_number, style active-danger

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:23:07 +02:00
Serreau Jovann
85220c6200 feat: ajout OrderNumberService pour generer les numeros de commande
src/Service/OrderNumberService.php (nouveau):
- Format: MM/YYYY-XXXXX (ex: 04/2026-00001, 04/2026-00002, etc.)
- Le compteur XXXXX est par mois: reset a 00001 a chaque nouveau mois
- generate(): cherche le dernier numOrder du mois courant via LIKE
  'MM/YYYY-%' ORDER BY DESC, extrait le compteur, incremente de 1,
  pad sur 5 chiffres, persiste et flush le nouvel OrderNumber
- generateAndUse(): genere + markAsUsed() en une operation
- preview(): retourne le prochain numero sans le creer en BDD

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:20:45 +02:00
Serreau Jovann
423ee779e0 feat: ajout entity OrderNumber pour la gestion des numeros de commande
src/Entity/OrderNumber.php (nouveau):
- id: int auto-increment
- numOrder: string(50) unique, le numero de commande
- createdAt: DateTimeImmutable, date de creation (auto dans le constructeur)
- isUsed: bool, false par defaut, marque via markAsUsed()

src/Repository/OrderNumberRepository.php (nouveau):
- Repository Doctrine standard pour OrderNumber

migrations/Version20260402201935.php:
- Table order_number avec index unique sur num_order
- created_at TIMESTAMP, is_used BOOLEAN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:19:54 +02:00
Serreau Jovann
a9057374d4 fix: deplacer le pool dns_infra_cache dans le bon fichier cache.yaml
config/packages/cache.yaml:
- Ajout du pool dns_infra_cache avec adapter cache.app et TTL 3600s
- Le pool etait dans config/packages/packages/cache.yaml qui est
  surcharge par config/packages/cache.yaml, donc le service
  dns_infra_cache n'existait pas

config/packages/packages/cache.yaml:
- Suppression du pool dns_infra_cache (doublon)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:17:29 +02:00
Serreau Jovann
28b84f09d4 feat: cache DNS report + purge EmailTracking + crons mis a jour
src/Controller/DnsReportController.php:
- Injection du pool cache dns_infra_cache via #[Autowire]
- Les resultats des checks sont caches avec la cle dns_infra_check_{token}
  pendant 1 heure (3600s) pour eviter de rappeler toutes les APIs
  (Cloudflare, AWS SES, Mailcow, RDAP, dig) a chaque rechargement
- La date du rapport est stockee dans le cache au format ISO 8601

config/packages/packages/cache.yaml:
- Nouveau pool dns_infra_cache sur Redis avec default_lifetime 3600s

src/Command/PurgeEmailTrackingCommand.php (nouveau):
- Commande app:email-tracking:purge qui supprime les EmailTracking
  dont sentAt est anterieur au seuil (90 jours par defaut)
- Option --days pour changer la retention (ex: --days=30)
- Utilise une requete DQL DELETE pour performance

ansible/deploy.yml.disabled:
- Nouveau cron "crm-siteconseil email-tracking purge": tous les jours
  a 5h du matin, supprime les EmailTracking de plus de 90 jours

docker/cron/entrypoint.sh:
- Liste complete des taches cron mise a jour avec:
  - */5 min: expire-pending, infra:snapshot
  - */15 min: services:check
  - toutes les heures: monitor:messenger
  - toutes les 2h: dns:check
  - toutes les 6h: stripe:sync
  - 3h: meilisearch consistency
  - 4h: attestations clean
  - 5h: email-tracking purge
  - 6h: cloudflare clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:16:50 +02:00
Serreau Jovann
c7a1c1b39f fix: corriger le serveur RDAP pour les domaines .dev
src/Service/DnsCheckService.php:
- RDAP_SERVERS['dev']: URL corrigee de rdap.nic.google (n'existe pas)
  vers pubapi.registry.google/rdap/domain/ (endpoint officiel Google
  Registry trouve via le bootstrap IANA data.iana.org/rdap/dns.json)
- esy-web.dev retourne maintenant: expiration 2028-01-08,
  NS candy.ns.cloudflare.com + nero.ns.cloudflare.com

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:13:34 +02:00
Serreau Jovann
a02e84eb26 feat: ajout verification WHOIS (nameservers Cloudflare + expiration domaine)
src/Service/DnsCheckService.php:
- Nouvelle methode checkWhois() qui verifie pour chaque domaine:
  - Nameservers via dig @1.1.1.1 NS: verifie que les NS contiennent
    *.ns.cloudflare.com, erreur si non Cloudflare
  - Expiration via RDAP (protocole standard ICANN remplacant whois):
    erreur si expire ou moins de 30 jours restants,
    warning si moins de 90 jours, OK sinon
- Constante RDAP_SERVERS avec les serveurs RDAP par TLD
  (.fr → rdap.nic.fr, .com → rdap.verisign.com, .dev → rdap.nic.google,
  .org/.eu → rdap.org, fallback rdap.org)
- Methode queryRdap() qui interroge le serveur RDAP du TLD avec
  Accept: application/rdap+json et timeout 10s
- Methode getNsRecords() qui parse la sortie dig NS
- Ajout du type NS dans le fallback dns_get_record

src/Command/CheckDnsCommand.php:
- Appel de checkWhois() apres checkBounce() pour chaque domaine

src/Controller/DnsReportController.php:
- Meme appel checkWhois() dans le rapport web

Seuils d'alerte:
- < 0 jours : erreur "EXPIRE"
- < 30 jours : erreur "X jours restants"
- < 90 jours : warning "X jours restants"
- > 90 jours : OK "X jours restants"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:11:32 +02:00
Serreau Jovann
f3ebcdaa85 fix: supprimer la ligne Mailcow DKIM du rapport DNS
src/Command/CheckDnsCommand.php:
- Suppression du check 'Mailcow DKIM: Ignore (DKIM via AWS SES)'
  et du succes associe (le DKIM est deja verifie par les 3 checks
  AWS SES DKIM CNAME, cette ligne etait redondante)

src/Controller/DnsReportController.php:
- Meme suppression dans le controller du rapport web

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:08:13 +02:00
Serreau Jovann
08f18d42fb fix: afficher les vraies valeurs dig et Cloudflare pour tous les sous-domaines
src/Command/CheckDnsCommand.php:
- Nouvelle methode getActualDnsValue() qui retourne la vraie valeur DNS
  selon le type (MX: liste des targets, CNAME: target, TXT: contenu,
  SRV: target:port) au lieu de "Trouve"/"Non trouve"
- Methodes getFirstTxtValue() et getSrvValue() pour recuperer les
  valeurs reelles TXT et SRV via dig @1.1.1.1
- Les checks Mailcow DNS utilisent maintenant getActualDnsValue()
  dans la colonne dig: mail.esy-web.dev, v=STSv1;id=..., etc.

src/Controller/DnsReportController.php (reecrit completement):
- Memes methodes enrichWithCloudflare() et enrichLastCheck() que la commande
- checkAwsSes(): enrichissement CF sur chaque DKIM CNAME
  ({token}._domainkey.{domain}), MAIL FROM MX et MAIL FROM TXT
- checkMailcow(): enrichissement CF sur chaque record attendu
  (autodiscover, autoconfig, SRV, _mta-sts, mta-sts)
- getActualDnsValue(), getMxValues(), getFirstTxtValue(), getSrvValue()
  pour afficher les vraies valeurs dig dans la colonne Dig
- Bounce: enrichissement CF sur bounce.{domain} MX
- La page web /email/configuration/{token} affiche maintenant les
  3 colonnes correctement remplies pour tous les enregistrements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:07:24 +02:00
Serreau Jovann
cf85c6b25a feat: enrichir la colonne Cloudflare pour tous les sous-domaines DNS
src/Command/CheckDnsCommand.php:
- Nouvelle methode enrichLastCheck() qui cherche un record dans les
  records Cloudflare par nom+type et remplit cloudflare/cf_status
  sur le dernier check ajoute au tableau
- Bounce: enrichissement CF sur bounce.{domain} type MX
- AWS SES DKIM CNAME: enrichissement CF sur {token}._domainkey.{domain}
  type CNAME pour chacun des 3 tokens DKIM
- AWS SES MAIL FROM MX: enrichissement CF sur bounce.{domain} type MX
- AWS SES MAIL FROM TXT: enrichissement CF sur bounce.{domain} type TXT
- Mailcow DNS: enrichissement CF pour chaque record attendu
  (autodiscover CNAME, autoconfig CNAME, _autodiscover._tcp SRV,
  _mta-sts TXT, mta-sts CNAME) avec le nom et type exacts
- checkAwsSes() et checkMailcow() recoivent maintenant $cfRecords
  en parametre pour effectuer les enrichissements

La colonne Cloudflare du rapport web et email affiche maintenant
la valeur presente dans la zone CF pour bounce, DKIM CNAME,
MAIL FROM, autodiscover, autoconfig, mta-sts au lieu de "Non trouve"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:04:43 +02:00
Serreau Jovann
74a7220fcd feat: ajout des crons DNS check et Cloudflare clean dans le deploy Ansible
ansible/deploy.yml.disabled:
- Nouveau cron "crm-siteconseil dns check": execute app:dns:check
  toutes les 2 heures (minute: 0, hour: */2), envoie le rapport
  DNS par email a monitor@siteconseil.fr et notification Discord
  si erreurs detectees, log dans /var/log/crm-siteconseil-dns-check.log
- Nouveau cron "crm-siteconseil cloudflare clean": execute
  app:cloudflare:clean tous les jours a 6h du matin (minute: 0,
  hour: 6), supprime les enregistrements TXT _acme-challenge
  obsoletes dans toutes les zones Cloudflare, log dans
  /var/log/crm-siteconseil-cloudflare-clean.log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:01:33 +02:00
Serreau Jovann
4fc14177d8 feat: commande app:cloudflare:clean pour lister les zones et nettoyer les _acme-challenge
src/Command/CloudflareDnsCleanCommand.php (nouveau):
- Commande app:cloudflare:clean avec 3 options:
  --list-only: affiche uniquement le tableau des zones (nom, ID, statut, plan)
  --zone=domain.fr: filtre sur une seule zone
  --dry-run: affiche les records a supprimer sans les supprimer
- Sans option: parcourt toutes les zones, trouve les TXT _acme-challenge
  et les supprime via l'API Cloudflare
- Affiche le nombre de records trouves et supprimes par zone
- Affiche le total global a la fin

src/Service/CloudflareService.php:
- listZones(): liste toutes les zones du compte avec pagination
  (retourne nom, id, status, plan pour chaque zone)
- deleteDnsRecord(): supprime un enregistrement DNS par zoneId + recordId
  via DELETE /zones/{zoneId}/dns_records/{recordId}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:01:05 +02:00
Serreau Jovann
8ef9711179 fix: paginer les records DNS Cloudflare pour recuperer tous les enregistrements
src/Service/CloudflareService.php - getDnsRecords():
- La zone esy-web.dev contient plus de 100 records DNS (26 TXT +
  MX + CNAME + A + AAAA etc.), la requete per_page=100 sans
  pagination ne retournait pas les TXT (SPF, DMARC, _mta-sts)
- Ajout d'une boucle de pagination: recupere page par page
  jusqu'a total_pages via result_info.total_pages
- Les colonnes Cloudflare du rapport affichaient "Non trouve"
  alors que les records existaient bien dans la zone

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:59:01 +02:00
Serreau Jovann
b36c4169e5 fix: utiliser EmailTrackingRepository par injection au lieu de doctrine service locator
src/Controller/DnsReportController.php:
- Remplacement de \$this->container->get('doctrine')->getManager()
  par injection de EmailTrackingRepository dans les parametres de __invoke()
- Le service locator du controller ne contient pas 'doctrine',
  l'injection de dependance est la bonne pratique Symfony

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:57:49 +02:00
Serreau Jovann
8b7591a6de feat: page web de rapport DNS detaille + simplification du mail
src/Controller/DnsReportController.php (nouveau):
- Route /email/configuration/{token} accessible via le lien dans le mail
- Utilise le messageId de l'EmailTracking comme token d'acces
  (seuls les destinataires du mail ont le lien)
- Execute tous les checks en temps reel: DnsCheckService (SPF, DMARC, MX,
  Bounce via dig @1.1.1.1), AwsSesService (domaine, 3 DKIM CNAME,
  MAIL FROM MX/TXT, bounce notif), CloudflareService (zone, records),
  MailcowService (domaine, DKIM, MX, autodiscover, autoconfig, SRV, MTA-STS)
- Enrichit chaque check avec la colonne Cloudflare
- Passe les resultats au template Twig pour affichage complet

templates/dns_report/index.html.twig (nouveau):
- Page glassmorphism avec header glass
- Resume en haut: 3 cards (verifications OK, erreurs, avertissements)
  avec bordures laterales colorees vert/rouge/jaune
- Tableau par domaine avec 6 colonnes: Source (badge colore par type:
  orange AWS, violet Mailcow, bleu Cloudflare, gris DNS), Verification,
  Attendu, Dig (actuel), Cloudflare, Statut (rond colore)
- Section erreurs detaillees avec liste
- Section avertissements avec liste
- Footer "Esy-Infra - Service de monitoring d'infra"

templates/emails/dns_report.html.twig (simplifie):
- Mail ne contient plus les details: seulement un tableau avec
  chaque domaine et son statut (OK vert / WARN jaune / KO rouge)
- Bouton "Voir le rapport complet" avec lien vers la page web
  (VML fallback pour Outlook)
- Le lien utilise le placeholder __DNS_REPORT_URL__ remplace par
  le MailerService avec le messageId du mail

src/Service/MailerService.php:
- Ajout du remplacement de __DNS_REPORT_URL__ par l'URL absolue
  /email/configuration/{messageId} dans sendEmail(), au meme
  endroit que __VIEW_URL__

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:57:28 +02:00
Serreau Jovann
6a071ffdf2 feat: forcer le resolver DNS 1.1.1.1 via dig + fallback dns_get_record
src/Service/DnsCheckService.php:
- Constante RESOLVER = '1.1.1.1' (Cloudflare DNS)
- Methode dig() utilise la commande dig @1.1.1.1 pour toutes les
  requetes DNS afin d'avoir des resultats coherents quel que soit
  le resolver local du serveur
- isDigAvailable(): detecte si dig est installe (cache static)
- fallbackDnsGetRecord(): quand dig n'est pas installe, utilise
  dns_get_record() PHP natif et formate la sortie au format dig
  +noall +answer pour que le parsing reste identique
- getTxtRecords(), getCnameRecord(), getMxRecords(), getSrvRecords()
  utilisent tous dig() en interne
- getCnameRecord() et getSrvRecords() rendues publiques pour utilisation
  par la commande

src/Command/CheckDnsCommand.php:
- Suppression du check DKIM generique (DKIM verifie uniquement via
  AWS SES avec les 3 CNAME individuels par domaine)
- checkDnsRecordExists(), checkMxExists(), checkTxtContains() utilisent
  maintenant $this->dnsCheck au lieu de dns_get_record() direct
- getCnameRecord() supprimee de la commande (delegue au service)
- getMxValues() et getTxtSpfValue() utilisent le service

docker/php/dev/Dockerfile:
- Ajout du paquet dnsutils (fournit la commande dig)

docker/php/prod/Dockerfile:
- Ajout du paquet dnsutils (fournit la commande dig)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:52:46 +02:00
Serreau Jovann
5d47db73d4 fix: corriger les checks Mailcow DNS et ignorer DKIM Mailcow
src/Service/MailcowService.php - getExpectedDnsRecords() reecrit:
- Suppression des checks SPF et DMARC (deja verifies par DnsCheckService)
- Suppression du check DKIM TXT (le DKIM est gere par AWS SES, pas Mailcow)
- Ajout du champ 'optional' (bool) dans chaque enregistrement attendu
  au lieu de deviner l'optionnalite par le nom
- Enregistrements verifies:
  - MX {domain} → mail.esy-web.dev (obligatoire)
  - CNAME autodiscover.{domain} → mail.esy-web.dev (obligatoire)
  - CNAME autoconfig.{domain} → mail.esy-web.dev (obligatoire)
  - SRV _autodiscover._tcp.{domain} (optionnel)
  - TXT _mta-sts.{domain} v=STSv1 (optionnel)
  - CNAME mta-sts.{domain} → mail.esy-web.dev (optionnel)

src/Command/CheckDnsCommand.php - checkMailcow():
- DKIM Mailcow marque comme OK avec detail "Ignore (DKIM via AWS SES)"
  car l'envoi des mails utilise le DKIM d'AWS SES, pas celui de Mailcow
- Suppression de la methode getDkimFromDns() devenue inutile
- Utilisation du champ 'optional' de getExpectedDnsRecords() au lieu
  de deviner par le nom de l'enregistrement

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:47:38 +02:00
Serreau Jovann
3313d28ef3 fix: corriger le parsing de l'API Mailcow pour getDomain et getDomainStatus
src/Service/MailcowService.php:
- getDomain(): l'API Mailcow retourne un objet JSON {} pour un seul
  domaine (pas un tableau [{...}]). Ajout de la detection: si $data[0]
  existe c'est un tableau, sinon c'est directement l'objet domaine
- getDomainStatus(): la cle du nombre de boites mail est
  'mboxes_in_domain' (pas 'mbox_count' qui n'existe pas dans la reponse)

Resultat: Mailcow affiche maintenant correctement:
- siteconseil.fr: actif, 28 boites mail
- esy-web.dev: actif, 20 boites mail
- Autodiscover, autoconfig, SRV, MTA-STS: tous OK pour les 2 domaines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:45:07 +02:00
Serreau Jovann
b9261f2946 feat: afficher les vraies valeurs AWS SES attendues dans le rapport DNS
src/Service/AwsSesService.php:
- Ajout methode getMailFromStatus() qui recupere via l'API SES:
  - mail_from_domain: le sous-domaine MAIL FROM (ex: bounce.siteconseil.fr)
  - mail_from_status: statut de verification (Success/Pending/Failed)
  - mx_expected: le MX attendu (feedback-smtp.{region}.amazonses.com)
  - txt_expected: le SPF attendu (v=spf1 include:amazonses.com ~all)
  Les valeurs sont specifiques a chaque domaine et region AWS

src/Command/CheckDnsCommand.php - methode checkAwsSes() reecrite:
- Verification domaine: attendu="Success", dig=statut reel
- DKIM statut global: attendu="Enabled=oui, Verified=oui", dig=statut reel
- 3 DKIM CNAME individuels: pour chaque token retourne par SES,
  verifie que {token}._domainkey.{domain} CNAME {token}.dkim.amazonses.com
  existe dans le DNS. Attendu=CNAME cible, Dig=valeur trouvee ou "Non trouve"
- MAIL FROM: attendu=sous-domaine configure dans SES, dig=statut
- MAIL FROM MX: attendu="{bounce.domain} MX feedback-smtp.{region}.amazonses.com",
  dig=MX reel trouve
- MAIL FROM TXT: attendu="{bounce.domain} TXT v=spf1 include:amazonses.com ~all",
  dig=enregistrement SPF reel trouve
- Bounce notifications: attendu="Forwarding ou SNS topic", dig=config reelle
- Ajout methodes getMxValues() et getTxtSpfValue() pour recuperer les
  valeurs reelles du DNS a afficher dans la colonne Dig

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:41:46 +02:00
Serreau Jovann
d1fdb5ab52 fix: corriger la verification de disponibilite Cloudflare
src/Service/CloudflareService.php:
- isAvailable(): remplace l'appel /user/tokens/verify par /zones?per_page=1
  car certains tokens API Cloudflare n'ont pas le scope User:Read
  necessaire pour /user/tokens/verify mais ont bien le scope Zone:DNS:Read
- Verifie maintenant que le champ 'success' est true dans la reponse

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:38:23 +02:00
Serreau Jovann
cdf09cab01 feat: ajout notification Discord en prod + mail priority HIGH pour le rapport DNS
src/Command/CheckDnsCommand.php:
- Injection de HttpClientInterface et kernel.environment dans le constructeur
- Constante DISCORD_WEBHOOK avec l'URL du webhook Discord
- sendReport(): appel sendEmail avec priority=1 (HIGH) pour que le mail
  soit marque comme urgent dans tous les clients mail
- sendDiscordNotification(): envoie un embed Discord avec:
  - Titre "Esy-Infra : ALERTE DNS" (rouge), "DNS avertissements" (jaune)
    ou "DNS Configuration OK" (vert)
  - Description avec le nombre de succes/erreurs/warnings
  - Les 5 premieres erreurs en citation markdown
  - Footer "Esy-Infra - Monitoring DNS" avec timestamp
- La notification Discord est envoyee UNIQUEMENT en environnement prod
  (condition sur kernel.environment == 'prod')

src/Service/MailerService.php:
- Ajout du parametre $priority (int, defaut 3 = normal) a sendEmail()
- Appel de ->priority($priority) sur l'objet Email Symfony
- Priority 1 = highest, 2 = high, 3 = normal, 4 = low, 5 = lowest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:37:10 +02:00
Serreau Jovann
c666e0db65 feat: enrichir le rapport DNS avec colonnes attendu/dig/cloudflare + envoi a monitor@siteconseil.fr
src/Service/DnsCheckService.php:
- Methode check() enrichie avec 4 nouveaux champs: expected (valeur attendue),
  dig (valeur actuelle trouvee par dig), cloudflare (valeur dans la zone CF),
  cf_status (statut de la colonne CF: ok/error/vide)
- checkSpf(): expected = "include:X dans le SPF", dig = contenu SPF complet
- checkDmarc(): expected = "p=reject ou p=quarantine", dig = contenu DMARC
- checkDkim(): expected = "FQDN CNAME/TXT", dig = cible CNAME ou debut TXT
- checkMx(): expected = MX attendu, dig = liste des MX trouves avec priorite
- checkBounce(): expected = "feedback-smtp.*.amazonses.com", dig = valeur trouvee

src/Command/CheckDnsCommand.php:
- Nouveau champ MONITOR_EMAIL = 'monitor@siteconseil.fr' pour l'envoi du rapport
- loadCloudflareRecords(): charge les records CF une seule fois par domaine
  au debut de l'execution, retourne un array indexe par domaine
- enrichWithCloudflare(): apres chaque check DNS, parcourt les records CF
  pour trouver l'enregistrement correspondant et remplir les colonnes
  cloudflare et cf_status dans chaque check
- checkAwsSes(): utilise DnsCheckService::check() avec expected/dig
  (ex: expected="Success", dig="Absent" pour la verification domaine)
- checkMailcow(): utilise DnsCheckService::check() avec expected/dig
  (ex: expected="Cle Mailcow: abc...", dig="Cle DNS: xyz..." pour DKIM)
- sendReport(): envoie a MONITOR_EMAIL au lieu de l'admin email

templates/emails/dns_report.html.twig:
- Tableau par domaine avec 6 colonnes: Type, Check, Attendu, Dig (actuel),
  Cloudflare, Statut (OK/erreur/warning)
- Colonne Dig coloree en vert/rouge/jaune selon le statut du check
- Colonne Cloudflare coloree selon cf_status
- Colonnes avec word-break: break-all pour les longues valeurs DNS
- Bandeau resume en haut avec compteurs succes/erreurs/warnings
  avec bordures laterales colorees
- Pied de mail: "Rapport par Esy-Infra - Service de monitoring d'infra"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:35:46 +02:00
Serreau Jovann
484c48e331 feat: ajout service Mailcow et integration dans la verification DNS
src/Service/MailcowService.php (nouveau):
- Connexion a l'API Mailcow via X-API-Key header
- getDomains(): liste tous les domaines configures
- getDomain(): informations d'un domaine specifique
- getDomainStatus(): statut actif, nombre de boites, quota, quota utilise
- getDkimKey(): recupere la cle DKIM TXT configuree dans Mailcow
- getExpectedDnsRecords(): retourne la liste des enregistrements DNS
  attendus par Mailcow pour un domaine (MX, SPF, DMARC, DKIM,
  autodiscover CNAME, autoconfig CNAME, SRV _autodiscover, _mta-sts TXT)
- getMailboxes(): liste les boites mail d'un domaine
- isAvailable(): test de connectivite API via /api/v1/get/status/containers

src/Command/CheckDnsCommand.php:
- Ajout de MailcowService dans le constructeur
- Nouvelle methode checkMailcow() qui:
  - Verifie si le domaine existe et est actif dans Mailcow
  - Recupere la cle DKIM Mailcow et la compare avec celle du DNS
    (comparaison partielle des 40 premiers caracteres)
  - Verifie chaque enregistrement DNS attendu par Mailcow:
    - MX, SPF, DMARC, DKIM : marques comme erreur si absents
    - autodiscover, autoconfig, SRV, _mta-sts : marques comme
      warning (optionnels)
- Methodes utilitaires: getDkimFromDns(), checkDnsRecordExists(),
  checkMxExists(), checkTxtContains(), getCnameRecord()

Variables d'environnement:
- .env: MAILCOW_URL=https://mail.esy-web.dev, MAILCOW_API_KEY (vide)
- .env.local: MAILCOW_API_KEY=DF0E7E-0FD059-16226F-8ECFF1-E558B3
- ansible/vault.yml: mailcow_api_key ajoutee
- ansible/env.local.j2: MAILCOW_URL et MAILCOW_API_KEY ajoutees

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:31:54 +02:00
Serreau Jovann
fea7dbfb61 feat: refactoring complet de la verification DNS avec services separes
Architecture:
- Les domaines (siteconseil.fr, esy-web.dev) sont definis en constante
  dans la commande uniquement, pas dans les services
- 3 services independants reutilisables:

src/Service/DnsCheckService.php (nouveau):
- Methodes publiques checkSpf(), checkDmarc(), checkDkim(), checkMx(),
  checkBounce() qui prennent le domaine en parametre
- Verification SPF: presence des includes amazonses.com et mail.esy-web.dev
- Verification DMARC: politique, presence de rua
- Verification DKIM: test de 10 selecteurs en CNAME et TXT
- Verification MX: le MX attendu est passe en parametre par la commande
- Verification Bounce: MX/CNAME/TXT sur bounce.*

src/Service/AwsSesService.php (nouveau):
- Authentification AWS Signature V4 via HTTP direct (pas de SDK)
- isDomainVerified(): verification du statut du domaine dans SES
- getDkimStatus(): statut DKIM (enabled, verified, tokens)
- getNotificationStatus(): bounce_topic, complaint_topic, forwarding
- listVerifiedIdentities(): liste des domaines verifies
- isAvailable(): test de connectivite API

src/Service/CloudflareService.php (nouveau):
- Authentification Bearer token via HTTP direct (pas de SDK)
- getZoneId(): recupere le zone ID dynamiquement par nom de domaine
  (plus besoin de CLOUDFLARE_ZONE_ID en dur)
- getDnsRecords(): tous les enregistrements d'une zone
- getDnsRecordsByType(): filtrage par type (TXT, MX, CNAME...)
- getZone(): informations d'une zone
- isAvailable(): verification du token API

src/Command/CheckDnsCommand.php (reecrit):
- Utilise les 3 services pour orchestrer les verifications
- Affichage console colore avec icones OK/ERREUR/ATTENTION
- Envoie un rapport email via le template Twig dns_report.html.twig

templates/emails/dns_report.html.twig (nouveau):
- Template email compatible tous clients (table-based, CSS inline,
  margin/padding longhand, mso-line-height-rule, pas de rgba/border-radius)
- Bandeau colore vert/jaune/rouge selon le statut global
- Section succes avec checkmarks verts dans un tableau alterne
- Section erreurs en rouge avec croix dans un tableau fond #fef2f2
- Section avertissements en jaune avec triangles fond #fffbeb
- Detail par domaine avec tableau type/verification/statut
- Utilise le template email/base.html.twig (header gold, footer dark)

Variables d'environnement ajoutees:
- .env: AWS_PK, AWS_SECRET, AWS_REGION (eu-west-3), CLOUDFLARE_KEY (vides)
- .env.local: valeurs reelles des cles AWS et Cloudflare
- ansible/vault.yml: aws_pk, aws_secret, cloudflare_key
- ansible/env.local.j2: AWS_PK, AWS_SECRET, AWS_REGION, CLOUDFLARE_KEY
  avec references au vault
- CLOUDFLARE_ZONE_ID supprime (recupere dynamiquement via l'API)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:28:24 +02:00
Serreau Jovann
b6696df087 feat: ajout commande app:dns:check pour verifier la configuration DNS
src/Command/CheckDnsCommand.php (nouveau fichier):
- Commande Symfony app:dns:check qui verifie les DNS de siteconseil.fr
  et esy-web.dev
- Verification SPF: presence des includes amazonses.com et mail.esy-web.dev,
  terminaison -all ou ~all
- Verification DMARC: presence sur _dmarc.*, politique p=reject/quarantine/none,
  presence de rua (adresse de rapport)
- Verification DKIM: test de 10 selecteurs (ses1/ses2/ses3, default, mail, k1,
  google, selector1/selector2, dkim) en CNAME et TXT
- Verification MX: presence de mail.esy-web.dev comme serveur de reception
- Verification Bounce: enregistrements MX/CNAME/TXT sur bounce.*,
  verification du SPF bounce avec include:amazonses.com
- Envoi d'un email de rapport complet a l'admin avec:
  - Bandeau colore vert (OK) / jaune (warnings) / rouge (erreurs)
  - Tableau des verifications reussies avec checkmarks verts
  - Tableau des erreurs en rouge avec croix
  - Tableau des avertissements en jaune avec triangles
  - Date du rapport et liste des domaines verifies
- Le rapport est envoye dans tous les cas (succes ou echec)
- Retourne FAILURE si au moins une erreur est detectee

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:22:45 +02:00
Serreau Jovann
2119d4be88 fix: dupliquer les styles du body sur la table wrapper dans le base email
Gmail, ProtonMail, SFR, Yahoo, Outlook.com, Orange et d'autres clients
email suppriment ou remplacent la balise <body> par un <div>, ce qui
fait perdre tous les styles inline definis sur le body.

templates/email/base.html.twig:
- Table wrapper principale: ajout de margin-top/right/bottom/left: 0,
  padding-top/right/bottom/left: 0, font-family: Arial, color: #111827,
  -webkit-text-size-adjust: 100%, -ms-text-size-adjust: 100%
  en plus du background-color: #eeeef3 deja present
- Les styles restent aussi sur le body pour les clients qui le supportent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:13:57 +02:00
Serreau Jovann
9ac03358f6 fix: remplacer les div spacers par des table spacers dans les emails
Outlook Windows (2007-2019) ne supporte pas la propriete height sur
les elements div, span et p. AOL et Yahoo remplacent height par
min-height ce qui casse le rendu.

templates/emails/revendeur_created.html.twig:
- 2 div spacers <div style="height: 8px;"> remplaces par des table
  spacers avec td font-size: 1px et mso-line-height-rule: exactly
  line-height: 8px + &nbsp; pour forcer la hauteur sur tous les clients

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:10:34 +02:00
Serreau Jovann
a3077ee5db feat: ajout commande app:mail:test + correction compatibilite text-decoration
src/Command/TestMailCommand.php (nouveau fichier):
- Commande Symfony app:mail:test <email> --mode=dev|prod
- Envoie un email de test complet pour verifier le rendu sur tous les clients
- Option --mode pour afficher un bandeau DEV (jaune) ou PROD (rouge)

templates/emails/test_mail.html.twig (nouveau fichier):
- Bandeau environnement colore (dev: #f59e0b jaune, prod: #dc2626 rouge)
- Bloc info sombre avec destinataire, environnement et date/heure
- Tableau de donnees avec 3 services (Esy-Web, Esy-Mail, Esy-Defender Pro),
  tarifs, statuts (ACTIF vert, IMPAYE rouge) et ligne TOTAL
- Liste de verification des styles: gras, italique, souligne, lien, couleurs
- Bloc alerte avec bordure laterale gold et fond gris
- 2 boutons centres (Gold #fabf04 et Dark #111827) avec fallback VML
  pour Outlook via v:roundrect
- Tableau des 5 applications SITECONSEIL avec liens et descriptions
- Simulation de fichier joint avec icone trombone
- Pied de mail avec mention de la commande utilisee
- Tout en CSS inline compatible: padding longhand, margin longhand,
  mso-line-height-rule:exactly, line-height en px, background-color,
  pas de rgba/border-radius/box-shadow

templates/emails/test_mail.html.twig:
- Balise <u> remplacee par span avec text-decoration: underline en inline
  (la balise <u> n'est pas supportee par ProtonMail, Orange iOS, SFR iOS)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:45:37 +02:00
Serreau Jovann
3dc94d67a6 feat: ajout des liens applications SITECONSEIL dans le webhook Discord
.gitea/workflows/discord-notify.yml:
- Ajout du champ "Applications SITECONSEIL" dans l'embed Discord
  avec 5 liens cliquables et leur description:
  - crm.siteconseil.fr: plateforme de gestion clients et revendeurs
  - sign.siteconseil.fr: signature electronique des documents (DocuSeal)
  - payment.siteconseil.fr: portail de paiement securise (Stripe)
  - status.siteconseil.fr: page de status et disponibilite des services
  - stripe.siteconseil.fr: reception des webhooks Stripe et redirection
    vers le dashboard Stripe Connect des revendeurs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:39:45 +02:00
Serreau Jovann
27cd61193b fix: corriger line-height et supprimer box-shadow dans les templates email
Outlook Windows (2007-2019) a un bug avec line-height en em et sans unite:
le rendu est imprevisible. La solution est d'utiliser des valeurs en px
avec mso-line-height-rule:exactly.

templates/email/base.html.twig:
- cellule contenu: line-height: 1.6 remplace par line-height: 22px
  avec ajout de mso-line-height-rule: exactly

templates/emails/2fa_code.html.twig:
- 3 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

templates/emails/forgot_password_code.html.twig:
- 3 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

templates/emails/membre_created.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
- 2 listes ul: line-height: 1.8 remplace par 25px (font 14px)
  et 23px (font 13px) + mso-line-height-rule

templates/emails/password_changed.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

templates/emails/revendeur_created.html.twig:
- 5 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

templates/emails/rgpd_access.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

templates/emails/rgpd_attestation_signed.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

templates/emails/rgpd_deletion.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

templates/emails/rgpd_no_data.html.twig:
- 5 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule

Suppression de box-shadow dans tous les templates email (non supporte
par Outlook Windows, Orange, GMX, WEB.DE, Samsung Email).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:37:37 +02:00
Serreau Jovann
1299d846f2 fix: remplacer padding shorthand par longhand dans tous les templates email
Outlook Windows (2007-2019) a un bug ou le padding vertical d'une cellule
est applique a toutes les cellules de la meme ligne avec la plus grande
valeur. SFR, Samsung Email, GMX, WEB.DE, HEY ne supportent que
partiellement le shorthand.

templates/emails/2fa_code.html.twig:
- span code: padding: 16px 32px remplace par padding-top/right/bottom/left

templates/emails/forgot_password_code.html.twig:
- span code: padding: 16px 32px remplace par padding-top/right/bottom/left

templates/emails/membre_created.html.twig:
- bloc identifiants: padding: 20px remplace par les 4 proprietes longhand
- 2 divs internes: padding: 4px 0 remplace par longhand
- bouton connexion: padding: 12px 24px remplace par longhand

templates/emails/revendeur_created.html.twig:
- bloc identifiants: padding: 20px remplace par longhand
- 3 blocs etapes: padding: 12px 16px remplace par longhand
- bouton mot de passe: padding: 14px 24px remplace par longhand
- bouton connexion: padding: 12px 24px remplace par longhand
- bloc info: padding: 16px remplace par longhand
- 3 divs internes: padding: 2px 0 remplace par longhand

templates/emails/rgpd_attestation_signed.html.twig:
- 3 blocs info: padding: 8px 12px remplace par longhand

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:36:05 +02:00
Serreau Jovann
361bb01463 fix: remplacer background shorthand, rgba() et border-radius dans les emails
Outlook Windows (2007-2019) ne supporte que background-color, pas
le shorthand background. rgba() et border-radius ne sont pas non plus
supportes par Outlook Windows Mail, GMX, Samsung Email, Orange.

templates/emails/2fa_code.html.twig:
- code de verification: background: #111827 remplace par background-color: #111827

templates/emails/forgot_password_code.html.twig:
- code de verification: background: #111827 remplace par background-color: #111827

templates/emails/membre_created.html.twig:
- bloc identifiants: background: #111827 remplace par background-color: #111827
- bouton connexion: background: #fabf04 remplace par background-color: #fabf04,
  border rgba(0,0,0,0.1) remplace par #e5e5e5, border-radius: 8px supprime

templates/emails/revendeur_created.html.twig:
- bloc identifiants: background: #111827 remplace par background-color: #111827
- 3 blocs etapes: background: #f9fafb remplace par background-color: #f9fafb
- bouton mot de passe: background: #fabf04 remplace par background-color: #fabf04,
  border rgba supprime, border-radius supprime
- bouton connexion: background: #fff remplace par background-color: #ffffff,
  border rgba supprime, border-radius supprime
- bloc info: background: #f9fafb remplace par background-color: #f9fafb

templates/emails/rgpd_attestation_signed.html.twig:
- 2 blocs info: background: #f9fafb remplace par background-color: #f9fafb

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:34:54 +02:00
Serreau Jovann
551fdfe7cf fix: rendre les templates email compatibles avec tous les clients mail
templates/email/base.html.twig:
- Remplacement de la structure div par une structure table-based (role=presentation)
  pour compatibilite Outlook/Orange/SFR/Yahoo
- Suppression de rgba(), linear-gradient(), border-radius, box-shadow
  (non supportes par Outlook Windows 2007-2019)
- Ajout du doctype XHTML 1.0 Transitional pour Outlook
- Ajout du bloc conditionnel <!--[if mso]> pour forcer Arial sur Outlook
- Remplacement de margin: 0 par margin-top/right/bottom/left: 0
  (margin shorthand non supporte par Orange, SFR, certains Outlook)
- Remplacement de padding shorthand par padding-top/right/bottom/left
  dans le body et les cellules principales
- Ajout de -webkit-text-size-adjust et -ms-text-size-adjust sur body
- Logo: ajout de width et height auto explicites + border: 0
- Fond semi-transparent rgba(255,255,255,0.92) remplace par #ffffff opaque
- Gradient gold remplace par background-color: #fabf04 fixe
- Couleur footer rgba(255,255,255,0.7) remplacee par #b0b0b0 fixe
- Entite &bull; remplacee par &#8226; pour compatibilite XHTML

templates/emails/*.html.twig (9 fichiers):
- Remplacement de toutes les proprietes margin shorthand par les
  proprietes longhand margin-top/margin-right/margin-bottom/margin-left
  dans 2fa_code, forgot_password_code, membre_created, password_changed,
  revendeur_created, rgpd_access, rgpd_attestation_signed, rgpd_deletion,
  rgpd_no_data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:40:13 +02:00
Serreau Jovann
e97116fc9d feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
  btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
  values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
  esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
  esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
  available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
  factures emises/payees/impayees cards, services renamed to Esy-*,
  rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
  set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
  mention_legal, rgpd, tarif): removed thick borders, font-black to
  font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
  container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
  font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
  lighter borders

Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
  (15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
  esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
  esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
  ensureRequiredGroups() method that checks existing groups and creates
  missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
  on page load, shows flash for each auto-created group, fetches user
  groups per member, passes availableGroups to template

Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
  (13 services), added factures_emises/payees/impayees KPI data

OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
  restored flash message on auth failure with error detail

Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
Serreau Jovann
f8155c9454 fix: update Keycloak group names for SITECONSEIL OAuth authentication
Problem: OAuth login failed because the authenticator was checking for
old Keycloak group names (super_admin_asso, gp_member) that no longer
exist in the master realm.

Changes:
- src/Security/KeycloakAuthenticator.php:106: resolveRoles() now checks
  for 'siteconseil_admin' instead of 'super_admin_asso' to grant ROLE_ROOT
- src/Controller/Admin/MembresController.php:140: member creation role
  resolution updated from 'super_admin_asso' to 'siteconseil_admin'
- templates/admin/membres.html.twig: checkbox values updated from
  'gp_member' to 'siteconseil_member' and 'super_admin_asso' to
  'siteconseil_admin' in the member management form
- assets/app.js:5-6: JS mutual exclusion logic updated to use new
  group values 'siteconseil_member' and 'siteconseil_admin'
- tests/Security/KeycloakAuthenticatorTest.php:79: test data updated
  from 'super_admin_asso' to 'siteconseil_admin'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:01:21 +02:00
Serreau Jovann
397867d882 feat: redesign entire CRM theme from brutalist to glassmorphism
Templates modified:
- templates/base.html.twig: header frosted glass (glass-heavy, backdrop-blur-24px),
  footer dark glass (glass-dark-heavy), cookie banner floating glass panel with
  rounded corners, all buttons converted to btn-glass/btn-gold/btn-dark classes,
  body background with radial gradient mesh (gold + indigo orbs), removed all
  border-4/border-8 thick borders, added rounded-lg corners on nav items
- templates/admin/_layout.html.twig: sidebar dark glass (glass-dark-heavy),
  nav items with sidebar-nav-item class (rounded-lg, hover glow), active items
  with gold glow shadow, avatar rounded-lg, dropdown borders changed to
  border-white/10, mobile overlay with backdrop-blur-4px
- templates/home/index.html.twig: login card with glass-heavy + glass-gold header,
  inputs with input-glass class (frosted blur, gold focus ring), buttons btn-gold
  with hover lift effect
- templates/security/login.html.twig: same glass treatment as home
- templates/security/2fa_*.html.twig: glass cards and inputs
- templates/security/forgot_password.html.twig: glass treatment
- templates/security/set_password*.html.twig: glass treatment
- templates/legal/_layout.html.twig: glass header
- templates/legal/tarif.html.twig: tabs converted to glass/glass-dark,
  all pricing cards glass/glass-gold, tables glass with rounded overflow
- templates/external_redirect.html.twig: glass card

SCSS (assets/app.scss):
- Added CSS custom properties: --glass-bg, --glass-border, --glass-blur,
  --gold, --gold-glow, --radius, --shadow-glass, etc.
- Added glass classes: .glass, .glass-heavy, .glass-dark, .glass-dark-heavy,
  .glass-gold (each with backdrop-filter, semi-transparent bg, subtle borders)
- Added button classes: .btn-glass, .btn-gold, .btn-dark (with hover lift,
  glow shadows, smooth cubic-bezier transitions)
- Added .input-glass (frosted input with gold focus ring)
- Added .sidebar-nav-item with .active/.active-danger states
- Added .glass-bg body class with radial gradient background
- Added custom scrollbar for sidebar
- Moved admin layout styles from inline <style> to SCSS

JavaScript (assets/app.js):
- Updated tarif tab classes from brutalist to glass

Config:
- .env.local: OAUTH_KEYCLOAK_REALM changed from siteconseil to master

Design direction: frosted glass panels over gradient mesh background,
semi-transparent surfaces, subtle 1px borders with white/20 opacity,
soft box-shadows, rounded-16px corners, smooth hover transitions with
translateY(-1px) lift effect, gold (#fabf04) accent glow shadows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:59:41 +02:00
Serreau Jovann
56ec1841d7 fix: rewrite Discord webhook workflow for Gitea compatibility
- Remove jq dependency
- Use file-based payload with cat > /tmp/discord.json instead of inline HEREDOC
- Fix HEREDOC indentation issue
- Escape JSON properly with sed
- Use curl -d @file for reliable payload delivery

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:51:33 +02:00
Serreau Jovann
6fa970e60d refactor: rebrand project to CRM SITECONSEIL (SARL SITECONSEIL)
- Rename all references from E-Cosplay/Ecosplay to SITECONSEIL
- Update entity from Association to SARL SITECONSEIL (Siret: 418664058)
- Update address to 27 rue Le Serurier, 02100 Saint-Quentin
- Update emails: contact@siteconseil.fr, rgpd@siteconseil.fr
- Update hosting from GCP to OVHcloud (Roubaix, Gravelines, Strasbourg, Paris)
- Update legal pages: mentions legales, CGV, RGPD, conformite, hebergement, cookies, CGU
- Add tarifs page with tabs: Site Internet, E-Commerce, Nom de domaine, Esy-Mail, Esy-Mailer, Esy-Tchat, Esy-Meet, Esy-Defender
- Add Discord webhook notification workflow
- Disable deploy and sonarqube workflows
- Update OAuth Keycloak realm to master
- Update logo references to logo_facture.png
- Remove forced image sizing in Liip Imagine filters
- Update SonarQube project key and badge token
- Update tribunal competent to Saint-Quentin
- Move tarif tabs JS to app.js (CSP compliance)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:48:25 +02:00
Serreau Jovann
363cea260b fix: replace all layout tables with CSS in email and PDF templates
- Email templates: replace table/tr/td with div-based CSS layout
- PDF templates: replace table with display:table/table-row/table-cell CSS (Dompdf compatible)
- Only table.data (RGPD access session data) remains as actual HTML table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:57:26 +02:00
Serreau Jovann
a76e96fb21 fix: add th headers and role=presentation to PDF and legal tables
- Add thead/th to commissions table in tarif.html.twig
- Add th headers to contrat_revendeur parties table
- Add role=presentation to PDF layout tables (info-grid, verify-box, signatures)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:50:56 +02:00
Serreau Jovann
242f8337e1 fix: replace deprecated HTML attributes and reduce JS nesting
- Replace email layout tables with CSS divs in base.html.twig
- Replace deprecated width/align attributes with CSS styles in PDF templates
- Add role="presentation" to email layout tables
- Convert td to th[scope=row] in profil and attestation verify tables
- Reduce function nesting in app.js by extracting renderHit/performSearch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:36:42 +02:00
Serreau Jovann
5f144ba4d2 fix: resolve SonarQube accessibility and test issues across templates
- Add for/id attributes to all form labels for accessibility compliance
- Add <title> tags to PDF templates (rgpd_access, rgpd_no_data, rgpd_deletion, contrat_revendeur)
- Add role="presentation" to email layout tables
- Remove deprecated cellpadding/cellspacing attributes from all templates
- Fix PHPUnit notices by replacing createMock with createStub where no expectations are set

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:30:53 +02:00
Serreau Jovann
b0060bd831 test: achieve 100% coverage for CheckServicesCommand and update SonarQube config
- Add 26 tests covering all service check types (HTTP, DocuSeal, Vault, Minio, Stripe)
- Include assets/ and templates/ in SonarQube sources
- Ignore php:S4144 globally (interface-imposed duplicate methods)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:24:15 +02:00
Serreau Jovann
409691fbad fix: reduce return count and suppress interface-imposed duplicate warnings
- ForgotPasswordController::validateResetInput: reduce from 4 to 3 returns using match expression
- DocuSealService::downloadSignedDocuments: reduce from 4 to 3 returns by extracting fetchAndStoreDocuments
- User: add @SuppressWarnings for getEmailAuthRecipient/getGoogleAuthenticatorUsername (required by 2FA interfaces)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:18:23 +02:00
Serreau Jovann
b0731a430c refactor: fix SonarQube code smells and add AnalyticsCryptoService tests
Reduce cognitive complexity and fix code smells across multiple files:
- Extract helper methods in DocuSealService, ForgotPasswordController, WebhookDocuSealController
- Reduce MembresController.persistLocalUser from 8 to 3 parameters using typed array
- Replace chained if/returns with ROLE_ROUTES map in LoginSuccessHandler
- Add 100% test coverage for AnalyticsCryptoService (15 tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:14:51 +02:00