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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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">✓ Active</span>
|
||||
{% else %}
|
||||
<span class="text-gray-400">✗ 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">✓ Active</span>
|
||||
{% else %}
|
||||
<span class="text-gray-400">✗ 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">
|
||||
|
||||
Reference in New Issue
Block a user