feat: onglet Securite dans fiche client

Onglet Securite (tab=securite) :

Statut du compte :
- Email, statut mot de passe (Temporaire jaune / Defini vert)
- 2FA Email (Active/Desactive), 2FA Google (Active/Desactive)

Changer le mot de passe :
- Formulaire avec nouveau mot de passe (min 8 chars)
- Hash via UserPasswordHasherInterface, clearTempPassword

Generer mot de passe temporaire :
- Genere 16 chars aleatoires, hash + setTempPassword
- Affiche le mot de passe en flash (pour renvoi email bienvenue)
- Modal confirmation avant action

Desactiver 2FA :
- Desactive 2FA Email + Google Authenticator + supprime secret + backup codes
- Bouton rouge avec modal confirmation
- Section visible uniquement si au moins 1 methode 2FA active

handleSecurityForm() :
- Actions : reset_password, disable_2fa, generate_temp_password

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-04 21:13:37 +02:00
parent 45972058ef
commit 42ab59ce07
2 changed files with 129 additions and 2 deletions

View File

@@ -15,6 +15,7 @@ use App\Service\OvhService;
use App\Service\UserManagementService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -350,7 +351,7 @@ class ClientsController extends AbstractController
}
#[Route('/{id}', name: 'show')]
public function show(Customer $customer, Request $request, EntityManagerInterface $em, OvhService $ovhService, CloudflareService $cloudflareService, DnsCheckService $dnsCheckService, EsyMailService $esyMailService): Response
public function show(Customer $customer, Request $request, EntityManagerInterface $em, OvhService $ovhService, CloudflareService $cloudflareService, DnsCheckService $dnsCheckService, EsyMailService $esyMailService, UserPasswordHasherInterface $passwordHasher): Response
{
$tab = $request->query->getString('tab', 'info');
@@ -371,6 +372,10 @@ class ClientsController extends AbstractController
return $this->handleDomainForm($request, $customer, $em, $ovhService, $cloudflareService, $dnsCheckService);
}
if ('POST' === $request->getMethod() && 'securite' === $tab) {
return $this->handleSecurityForm($request, $customer, $em, $passwordHasher);
}
$this->ensureDefaultContact($customer, $em);
$contacts = $em->getRepository(\App\Entity\CustomerContact::class)->findBy(['customer' => $customer], ['createdAt' => 'DESC']);
$domains = $em->getRepository(\App\Entity\Domain::class)->findBy(['customer' => $customer]);
@@ -516,6 +521,44 @@ class ClientsController extends AbstractController
}
}
private function handleSecurityForm(Request $request, Customer $customer, EntityManagerInterface $em, UserPasswordHasherInterface $passwordHasher): Response
{
$action = $request->request->getString('security_action');
$user = $customer->getUser();
if ('reset_password' === $action) {
$newPassword = $request->request->getString('new_password');
if (\strlen($newPassword) < 8) {
$this->addFlash('error', 'Le mot de passe doit contenir au moins 8 caracteres.');
} else {
$user->setPassword($passwordHasher->hashPassword($user, $newPassword));
$user->clearTempPassword();
$em->flush();
$this->addFlash('success', 'Mot de passe modifie.');
}
}
if ('disable_2fa' === $action) {
$user->setIsEmailAuthEnabled(false);
$user->setIsGoogleAuthEnabled(false);
$user->setGoogleAuthenticatorSecret(null);
$user->setBackupCodes([]);
$em->flush();
$this->addFlash('success', 'Authentification a deux facteurs desactivee.');
}
if ('generate_temp_password' === $action) {
$tempPassword = bin2hex(random_bytes(8));
$user->setPassword($passwordHasher->hashPassword($user, $tempPassword));
$user->setTempPassword($tempPassword);
$em->flush();
$this->addFlash('success', 'Mot de passe temporaire genere : '.$tempPassword.' — Le client devra le changer a sa prochaine connexion.');
}
return $this->redirectToRoute('app_admin_clients_show', ['id' => $customer->getId(), 'tab' => 'securite']);
}
#[Route('/{id}/resend-welcome', name: 'resend_welcome', methods: ['POST'])]
public function resendWelcome(Customer $customer, MailerService $mailer, Environment $twig): Response
{

View File

@@ -43,7 +43,8 @@
'ndd': 'Noms de domaine',
'esyflex': 'EsyFlex',
'sites': 'Sites Internet',
'services': 'Services'
'services': 'Services',
'securite': 'Securite'
} %}
<div class="flex flex-wrap gap-1 mb-6">
@@ -454,6 +455,89 @@
<div class="glass p-8 text-center text-gray-400 font-bold">Aucun site internet.</div>
{% endif %}
{# Tab: Securite #}
{% elseif tab == 'securite' %}
{% set user = customer.user %}
{# Statut compte #}
<section class="glass p-6 mb-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Statut du compte</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Email</span>
<span class="font-mono font-bold">{{ user.email }}</span>
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Mot de passe</span>
{% if user.hasTempPassword %}
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 font-bold uppercase text-[10px] rounded">Temporaire</span>
{% else %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">Defini</span>
{% endif %}
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">2FA Email</span>
{% if user.isEmailAuthEnabled %}
<span class="text-green-600 font-bold">&#10003; Active</span>
{% else %}
<span class="text-gray-400">&#10007; Desactive</span>
{% endif %}
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">2FA Google</span>
{% if user.isGoogleAuthenticatorEnabled %}
<span class="text-green-600 font-bold">&#10003; Active</span>
{% else %}
<span class="text-gray-400">&#10007; Desactive</span>
{% endif %}
</div>
</div>
</section>
{# Changer le mot de passe #}
<section class="glass p-6 mb-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Changer le mot de passe</h2>
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'securite'}) }}" class="flex flex-wrap items-end gap-4">
<input type="hidden" name="security_action" value="reset_password">
<div class="flex-1 min-w-[250px]">
<label for="new_password" class="block text-xs font-bold uppercase tracking-wider mb-2">Nouveau mot de passe *</label>
<input type="password" id="new_password" name="new_password" required minlength="8" placeholder="Min. 8 caracteres"
class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<button type="submit" class="px-5 py-3 btn-gold text-xs font-bold uppercase tracking-wider text-gray-900">Modifier le mot de passe</button>
</form>
</section>
{# Generer mot de passe temporaire #}
<section class="glass p-6 mb-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-sm font-bold uppercase tracking-wider">Generer un mot de passe temporaire</h2>
<p class="text-[10px] text-gray-400 mt-1">Genere un nouveau mot de passe et l'affiche. Le client devra le changer a sa prochaine connexion. Utile pour renvoyer un email de bienvenue.</p>
</div>
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'securite'}) }}" data-confirm="Generer un nouveau mot de passe temporaire ? L'ancien mot de passe sera invalide.">
<input type="hidden" name="security_action" value="generate_temp_password">
<button type="submit" class="px-5 py-3 glass text-indigo-600 font-bold uppercase text-xs tracking-wider hover:bg-indigo-600 hover:text-white transition-all">Generer</button>
</form>
</div>
</section>
{# Desactiver 2FA #}
{% if user.isEmailAuthEnabled or user.isGoogleAuthenticatorEnabled %}
<section class="glass p-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-sm font-bold uppercase tracking-wider">Desactiver l'authentification a deux facteurs</h2>
<p class="text-[10px] text-gray-400 mt-1">Desactive le 2FA par email et Google Authenticator. Le client pourra le reactiver depuis son profil.</p>
</div>
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'securite'}) }}" data-confirm="Desactiver la double authentification pour {{ customer.fullName }} ?">
<input type="hidden" name="security_action" value="disable_2fa">
<button type="submit" class="px-5 py-3 border-2 border-red-600 bg-white text-red-600 font-bold uppercase text-xs tracking-wider hover:bg-red-600 hover:text-white transition-all" style="border-radius: 6px;">Desactiver 2FA</button>
</form>
</div>
</section>
{% endif %}
{# Tabs placeholder #}
{% else %}
<div class="glass p-12 text-center">