feat(BackupCommand): Améliore la sauvegarde avec gestion des erreurs et nettoyage.

🐛 fix(SyncCommand): Corrige la synchronisation DNS et gère les erreurs OVH.
⚙️ refactor(DemandeCommand): Refactorise la génération du fichier hosts.ini.
🧹 chore(CustomerCommand): Purge les clients supprimés et leurs dépendances.
 test(TestMailerCommand): Ajoute une commande pour tester l'envoi d'emails.
 feat(run.sh): Ajoute un script pour exécuter les commandes de demande.
 feat(EmailCommand): Supprime les emails Mailcow marqués comme supprimés.
 feat(AccountCommand): Crée un utilisateur admin si inexistant.
 feat(ExportComptable): Initialise la commande d'export comptable.
This commit is contained in:
Serreau Jovann
2025-09-27 16:38:57 +02:00
parent 3057080b52
commit f89d9ba30a
10 changed files with 226 additions and 157 deletions

View File

@@ -194,7 +194,13 @@
args:
chdir: "{{ path }}"
when: ansible_os_family == "Debian" # Added a when condition here, often missed
- name: "Execute created subcriber link"
cron:
name: "Mainframe - subcriber link"
minute: "0"
hour: "*"
job: "sh {{ path }}/script/demande/run.sh"
user: root
- name: "Cron Task purge customer delete"
cron:
name: "Mainframe - Purge customer"
@@ -202,6 +208,7 @@
hour: "21"
job: "php {{ path }}/bin/console mainframe:cron:customer"
user: root
- name: "Cron Task purge email delete"
cron:
name: "Mainframe - Purge customer"
@@ -209,6 +216,7 @@
hour: "21"
job: "php {{ path }}/bin/console mainframe:cron:email"
user: root
- name: "Cron Task sync"
ansible.builtin.cron:
name: "Mainframe - Sync"

5
script/demande/run.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
php /var/www/mainframe/app/bin/console mainframe:demande
ansible-playbook -i script/demande/hosts.ini script/demande/playbook.yaml

View File

@@ -19,8 +19,12 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[AsCommand(name: 'mainframe:admin')]
class AccountCommand extends Command
{
public function __construct(private readonly EventDispatcherInterface $eventDispatcher, private readonly UserPasswordHasherInterface $userPasswordHasher, private readonly EntityManagerInterface $entityManager, ?string $name = null)
{
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly UserPasswordHasherInterface $userPasswordHasher,
private readonly EntityManagerInterface $entityManager,
?string $name = null
) {
parent::__construct($name);
}
@@ -29,28 +33,32 @@ class AccountCommand extends Command
$io = new SymfonyStyle($input, $output);
$io->title("Création d'un utilisateur administrateur");
$userExit = $this->entityManager->getRepository(Account::class)->findOneBy(['email'=>'jovann@siteconseil.fr']);
if(!$userExit instanceof Account) {
$existingUser = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => 'jovann@siteconseil.fr']);
if (!$existingUser instanceof Account) {
$password = TempPasswordGenerator::generate();
$userExit = new Account();
$userExit->setRoles(['ROLE_ROOT']);
$userExit->setUuid(Uuid::v4());
$userExit->setIsActif(true);
$userExit->setIsFirstLogin(true);
$newUser = new Account();
$newUser->setRoles(['ROLE_ROOT']);
$newUser->setUuid(Uuid::v4());
$newUser->setIsActif(true);
$newUser->setIsFirstLogin(true);
$questionEmail = new Question("Email ?");
$email = $io->askQuestion($questionEmail);
$email = $io->askQuestion(new Question("Email ?"));
$newUser->setEmail($email);
$userExit->setEmail($email);
$username = $io->askQuestion(new Question("Username ?"));
$newUser->setUsername($username);
$questionUsername = new Question("Username ?");
$username = $io->askQuestion($questionUsername);
$userExit->setUsername($username);
$userExit->setPassword($this->userPasswordHasher->hashPassword($userExit, $password));
$hashedPassword = $this->userPasswordHasher->hashPassword($newUser, $password);
$newUser->setPassword($hashedPassword);
$this->entityManager->persist($usserExit);
$this->entityManager->persist($newUser);
$this->entityManager->flush();
$this->eventDispatcher->dispatch(new CreatedAdminEvent($userExit, $password));
$this->eventDispatcher->dispatch(new CreatedAdminEvent($newUser, $password));
$io->success("Utilisateur administrateur créé avec succès.");
} else {
$io->warning("Un utilisateur avec l'email 'jovann@siteconseil.fr' existe déjà.");
}
return Command::SUCCESS;

View File

@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Command;
use App\Entity\Revendeur;
use App\Repository\RevendeurRepository;
use App\Service\Revendeur\RevendeurService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -18,12 +16,11 @@ use Symfony\Component\HttpKernel\KernelInterface;
#[AsCommand(name: 'mainframe:backup', description: 'Backup command')]
class BackupCommand extends Command
{
public function __construct(
private readonly KernelInterface $kernelInterface,
private readonly KernelInterface $kernel,
private readonly RevendeurService $revendeurService,
?string $name = null)
{
?string $name = null
) {
parent::__construct($name);
}
@@ -32,88 +29,102 @@ class BackupCommand extends Command
$io = new SymfonyStyle($input, $output);
$io->title('Backup command');
$path = $this->kernelInterface->getProjectDir()."/backup";
$this->backuped($path);
$backupPath = $this->kernel->getProjectDir() . '/backup';
if (!is_dir($backupPath) && !mkdir($backupPath, 0755, true) && !is_dir($backupPath)) {
$io->error(sprintf('Impossible de créer le dossier de sauvegarde %s', $backupPath));
return Command::FAILURE;
}
try {
$this->performBackup($backupPath);
} catch (\Exception $e) {
$io->error('Erreur lors de la sauvegarde : ' . $e->getMessage());
return Command::FAILURE;
}
// Nettoyage des fichiers anciens (> 7 jours)
$finder = new Finder();
$files = $finder->in($path)->files()->name('*.tar.gz');
$finder->files()->in($backupPath)->name('*.tar.gz');
$now = time();
foreach ($files as $file) {
if ($now - $file->getMTime() > 60 * 60 * 24 * 7) {
foreach ($finder as $file) {
if (($now - $file->getMTime()) > 60 * 60 * 24 * 7) {
@unlink($file->getRealPath());
}
}
$io->success('Sauvegarde terminée avec succès.');
return Command::SUCCESS;
}
private function backuped(string $backupPath): void
private function performBackup(string $backupPath): void
{
$database= $_ENV['DATABASE_URL'];
$databaseUrl = $_ENV['DATABASE_URL'] ?? '';
preg_match(
'/^postgres(?:ql)?:\\/\\/(.*?):(.*?)@(.*?):(\\d+)\\/(.*?)(?:\\?|$)/',
$database,
'/^postgres(?:ql)?:\/\/(.*?):(.*?)@(.*?):(\d+)\/(.*?)(?:\?|$)/',
$databaseUrl,
$matches
);
// À adapter selon ta configuration/env Symfony !
$user = $matches[1] ?? null;
$password = $matches[2] ?? null;
$host = $matches[3] ?? null;
$port = $matches[4] ?? null;
$db = $matches[5] ?? null;
if (count($matches) < 6) {
throw new \RuntimeException('DATABASE_URL invalide ou non configurée.');
}
[$user, $password, $host, $port, $db] = array_slice($matches, 1, 5);
$sqlFilename = sprintf('%s/pgsql_backup_%s.sql', $backupPath, date('Y-m-d_His'));
// Pour éviter les exposes de mdp dans commande : passer par env
putenv("PGPASSWORD={$password}");
// Option -F c pour backup custom compressé, -f pour destination
$command = sprintf(
'pg_dump -h %s -U %s -F c %s -f %s',
$host,
$user,
$db,
$sqlFilename
escapeshellarg($host),
escapeshellarg($user),
escapeshellarg($db),
escapeshellarg($sqlFilename)
);
// Exécution du backup
exec($command);
exec($command, $output, $returnVar);
if ($returnVar !== 0) {
throw new \RuntimeException('Erreur lors de l\'exécution de pg_dump.');
}
$zipFilename = sprintf('%s/backup_%s.zip', $backupPath, date('d-m-Y'));
// Création du ZIP et ajout du SQL dedans
$zip = new \ZipArchive();
if ($zip->open($zipFilename, \ZipArchive::CREATE) === true) {
// Le fichier dans le zip portera juste le nom du .sql
$zip->addFile($sqlFilename, basename($sqlFilename));
$vichUploadDirs = [
$this->kernelInterface->getProjectDir() . '/public/storage', // Exemple chemin upload
// ajouter dautres chemins si besoin
];
foreach ($vichUploadDirs as $dir) {
$this->addDirToZip($zip, $dir, basename($dir));
}
$zip->close();
unlink($sqlFilename);
if ($zip->open($zipFilename, \ZipArchive::CREATE) !== true) {
throw new \RuntimeException("Impossible de créer l'archive ZIP : $zipFilename");
}
$zip->addFile($sqlFilename, basename($sqlFilename));
$uploadDirs = [
$this->kernel->getProjectDir() . '/public/storage', // Ajouter d'autres chemins si nécessaire
];
foreach ($uploadDirs as $dir) {
$this->addDirectoryToZip($zip, $dir, basename($dir));
}
$zip->close();
@unlink($sqlFilename);
}
private function addDirToZip(\ZipArchive $zip, string $folder, string $zipPath): void
private function addDirectoryToZip(\ZipArchive $zip, string $folderPath, string $zipPath): void
{
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($folder),
new \RecursiveDirectoryIterator($folderPath, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $name => $file) {
if (!$file->isDir()) {
foreach ($files as $file) {
if ($file->isFile()) {
$filePath = $file->getRealPath();
$relativePath = $zipPath . '/' . substr($filePath, strlen($folder) + 1);
$relativePath = $zipPath . '/' . substr($filePath, strlen($folderPath) + 1);
$zip->addFile($filePath, $relativePath);
}
}
}
}

View File

@@ -2,48 +2,51 @@
namespace App\Command;
use App\Entity\Account;
use App\Entity\Customer;
use App\Service\Generator\TempPasswordGenerator;
use App\Service\Mailer\Event\CreatedAdminEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[AsCommand(name: 'mainframe:cron:customer')]
class CustomerCommand extends Command
{
public function __construct(private readonly EventDispatcherInterface $eventDispatcher, private readonly EntityManagerInterface $entityManager, ?string $name = null)
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
?string $name = null
) {
parent::__construct($name);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title("Purge all customer delete");
foreach ($this->entityManager->getRepository(Customer::class)->findBy(['isDeleted'=>true]) as $delete) {
$io->info("Delete account - ".$delete->getRaisonSocial());
foreach ($delete->getCustomerContacts()as $customerContact) {
$this->entityManager->remove($customerContact);
$io->title('Purge all deleted customers');
$customersToDelete = $this->entityManager->getRepository(Customer::class)->findBy(['isDeleted' => true]);
foreach ($customersToDelete as $customer) {
$io->info(sprintf('Deleting customer: %s', $customer->getRaisonSocial()));
foreach ($customer->getCustomerContacts() as $contact) {
$this->entityManager->remove($contact);
}
foreach ($delete->getCustomerAdvertPayments() as $customerAdvertPayment) {
$this->entityManager->remove($customerAdvertPayment);
foreach ($customer->getCustomerAdvertPayments() as $advertPayment) {
$this->entityManager->remove($advertPayment);
}
foreach ($delete->getCustomerDevis() as $customerDevis) {
$this->entityManager->remove($customerDevis);
foreach ($customer->getCustomerDevis() as $devis) {
$this->entityManager->remove($devis);
}
$this->entityManager->remove($delete);
$this->entityManager->flush();
$this->entityManager->remove($customer);
}
$this->entityManager->flush();
$io->success('Purge complete.');
return Command::SUCCESS;
}
}

View File

@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Command;
use App\Entity\Revendeur;
use App\Repository\RevendeurRepository;
use App\Service\Revendeur\RevendeurService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -16,29 +14,38 @@ use Symfony\Component\HttpKernel\KernelInterface;
#[AsCommand(name: 'mainframe:demande', description: 'Command generate for hosted file')]
class DemandeCommand extends Command
{
public function __construct(
private readonly KernelInterface $kernelInterface,
private readonly KernelInterface $kernel,
private readonly RevendeurService $revendeurService,
?string $name = null)
{
?string $name = null
) {
parent::__construct($name);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$hosts =[];
$hosts[] = "[local_hosts]";
/** @var Revendeur $revendeur */
$lines = ['[local_hosts]'];
foreach ($this->revendeurService->list() as $revendeur) {
$hosts[] =$revendeur->getCode()."-demande ansible_host=127.0.0.1 path=".$revendeur->getCode()."-demande";
$code = $revendeur->getCode();
$lines[] = sprintf('%s-demande ansible_host=127.0.0.1 path=%s-demande', $code, $code);
}
$pathFile = $this->kernelInterface->getProjectDir()."/script/demande/hosts.ini";
if(file_exists($pathFile)) {
unlink($pathFile);
$pathFile = $this->kernel->getProjectDir() . '/script/demande/hosts.ini';
if (file_exists($pathFile) && !unlink($pathFile)) {
$output->writeln("<error>Impossible de supprimer le fichier existant : {$pathFile}</error>");
return Command::FAILURE;
}
file_put_contents($pathFile, implode("\n", $hosts));
$result = file_put_contents($pathFile, implode("\n", $lines));
if ($result === false) {
$output->writeln("<error>Échec de l'écriture du fichier : {$pathFile}</error>");
return Command::FAILURE;
}
$output->writeln("<info>Fichier hosts.ini généré avec succès à l'emplacement : {$pathFile}</info>");
return Command::SUCCESS;
}
}

View File

@@ -2,49 +2,53 @@
namespace App\Command;
use App\Entity\Account;
use App\Entity\Customer;
use App\Entity\CustomerDnsEmail;
use App\Service\Generator\TempPasswordGenerator;
use App\Service\Mailer\Event\CreatedAdminEvent;
use Doctrine\ORM\EntityManagerInterface;
use Exbil\MailCowAPI;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[AsCommand(name: 'mainframe:cron:email')]
class EmailCommand extends Command
{
public function __construct(private readonly EventDispatcherInterface $eventDispatcher, private readonly EntityManagerInterface $entityManager, ?string $name = null)
{
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly EntityManagerInterface $entityManager,
?string $name = null
) {
parent::__construct($name);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title("Purge all mail delete");
foreach ($this->entityManager->getRepository(CustomerDnsEmail::class)->findBy(['isDeleted'=>true]) as $delete) {
$io->info("Delete account - ".$delete->getEmail());
$client = new MailCowAPI('mail.esy-web.dev',$_ENV['MAILCOW_KEY']);
$email = $delete->getEmail()."@".$delete->getDns()->getNdd();
$io->title('Purge des emails supprimés');
$mailcow = new MailCowAPI('mail.esy-web.dev', $_ENV['MAILCOW_KEY']);
$emailsToDelete = $this->entityManager->getRepository(CustomerDnsEmail::class)->findBy(['isDeleted' => true]);
foreach ($emailsToDelete as $emailEntity) {
$fullEmail = $emailEntity->getEmail() . '@' . $emailEntity->getDns()->getNdd();
$io->info("Suppression boîte mail : $fullEmail");
try {
$client->mailBoxes()->deleteMailBox([$email]);
$mailcow->mailBoxes()->deleteMailBox([$fullEmail]);
} catch (\Exception $e) {
$io->error($e);
$io->error("Erreur lors de la suppression de $fullEmail : " . $e->getMessage());
// Optionnel : continuer ou interrompre en fonction du besoin
}
$this->entityManager->remove($delete);
$this->entityManager->remove($emailEntity);
}
$this->entityManager->flush();
$io->success('Purge terminée avec succès.');
return Command::SUCCESS;
}
}

View File

@@ -1,13 +1,14 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'mainframe:export')]
#[AsCommand(name: 'mainframe:export', description: 'Export comptable')]
class ExportComptable extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -15,8 +16,18 @@ class ExportComptable extends Command
$io = new SymfonyStyle($input, $output);
$io->title('Export comptable');
$lines =[];
$lines[] = ['JournalCode','JournalLib','EcritureNum','EcritureDate','CompteNum','CompteLib','CompAuxNum','CompAuxLib','PieceRef','PieceDate','EcritureLib','Debit','Credit','EcritureLet','DateLet','ValidDate','Montantdevise','Idevise','DateRglt','ModeRglt','NatOp','IdClient'];
// En-têtes de colonnes pour export CSV/comptable
$lines = [];
$lines[] = [
'JournalCode', 'JournalLib', 'EcritureNum', 'EcritureDate', 'CompteNum', 'CompteLib',
'CompAuxNum', 'CompAuxLib', 'PieceRef', 'PieceDate', 'EcritureLib', 'Debit', 'Credit',
'EcritureLet', 'DateLet', 'ValidDate', 'Montantdevise', 'Idevise', 'DateRglt',
'ModeRglt', 'NatOp', 'IdClient'
];
// TODO: Ajouter ici la logique de génération des lignes dexport
$io->success('Export comptable initialisé. À compléter.');
return Command::SUCCESS;
}

View File

@@ -2,48 +2,60 @@
namespace App\Command;
use App\Entity\Account;
use App\Entity\CustomerDns;
use App\Service\Generator\TempPasswordGenerator;
use App\Service\Mailer\Event\CreatedAdminEvent;
use App\Service\Ovh\Client;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[AsCommand(name: 'mainframe:cron:sync')]
#[AsCommand(name: 'mainframe:cron:sync', description: 'Synchronise les données DNS')]
class SyncCommand extends Command
{
public function __construct(private readonly EntityManagerInterface $entityManager,private readonly Client $ovhClient,?string $name = null)
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly Client $ovhClient,
?string $name = null
) {
parent::__construct($name);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title("Sync Data");
//sync dns
foreach ($this->entityManager->getRepository(CustomerDns::class)->findAll() as $customerDns) {
$io->info('Sync DNS - '.$customerDns->getDns());
if($customerDns->getRegistar() == "ovh") {
$data = $this->ovhClient->info($customerDns->getNdd());
if(!is_null($data)){
$d = \DateTime::createFromFormat(\DateTime::ATOM,$data['expired']);
$customerDns->setExpiredAt($d);
$this->entityManager->persist($customerDns);
}
$io->title('Synchronisation des données DNS');
$dnsRepository = $this->entityManager->getRepository(CustomerDns::class);
$customerDnsList = $dnsRepository->findAll();
foreach ($customerDnsList as $customerDns) {
$io->info(sprintf('Synchronisation DNS pour : %s', $customerDns->getDns()));
if ($customerDns->getRegistar() !== 'ovh') {
continue;
}
try {
$data = $this->ovhClient->info($customerDns->getNdd());
if ($data !== null && isset($data['expired'])) {
$expiredAt = \DateTime::createFromFormat(\DateTime::ATOM, $data['expired']);
if ($expiredAt !== false) {
$customerDns->setExpiredAt($expiredAt);
$this->entityManager->persist($customerDns);
$io->writeln('Date dexpiration mise à jour : ' . $expiredAt->format('Y-m-d'));
} else {
$io->warning('Format de date invalide pour ' . $customerDns->getNdd());
}
}
} catch (\Exception $e) {
$io->error(sprintf('Erreur lors de la synchro DNS pour %s : %s', $customerDns->getNdd(), $e->getMessage()));
}
}
$this->entityManager->flush();
$io->success('Synchronisation terminée');
return Command::SUCCESS;
}

View File

@@ -2,35 +2,35 @@
namespace App\Command;
use App\Entity\Account;
use App\Service\Mailer\Event\CreatedAdminEvent;
use App\Service\Mailer\Mailer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[AsCommand(name: 'mainframe:testmail')]
#[AsCommand(name: 'mainframe:testmail', description: 'Commande de test pour l\'envoi de mail')]
class TestMailerCommand extends Command
{
public function __construct(private readonly Mailer $mailer, ?string $name = null)
{
public function __construct(
private readonly Mailer $mailer,
?string $name = null
) {
parent::__construct($name);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title("TEST MAILER");
$_ENV['REAL_MAIL'] = "1";
$this->mailer->sendTest();
$io->title('Test Mailer');
try {
$this->mailer->sendTest();
$io->success('Test d\'envoi de mail effectué avec succès.');
} catch (\Exception $e) {
$io->error('Erreur lors de l\'envoi du mail : ' . $e->getMessage());
return Command::FAILURE;
}
return Command::SUCCESS;
}