Add invitation registration flow: accept sends email, prefilled signup form
- Accept: sends email with unique registration link
- /invitation/{token}/inscription: prefilled form (company, email, offer, commission)
with password, SIRET, address, phone fields
- Account created as ROLE_ORGANIZER, pre-approved, pre-verified
- Response page: link to finalize registration immediately
- Email: welcome message with offer recap and register button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ use App\Service\EventIndexService;
|
||||
use App\Service\MailerService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -257,7 +258,7 @@ class HomeController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/invitation/{token}/{action}', name: 'app_invitation_respond', requirements: ['action' => 'accept|refuse'], methods: ['POST'])]
|
||||
public function respondInvitation(string $token, string $action, EntityManagerInterface $em): Response
|
||||
public function respondInvitation(string $token, string $action, EntityManagerInterface $em, MailerService $mailerService): Response
|
||||
{
|
||||
$invitation = $em->getRepository(OrganizerInvitation::class)->findOneBy(['token' => $token]);
|
||||
if (!$invitation || !\in_array($invitation->getStatus(), [OrganizerInvitation::STATUS_SENT, OrganizerInvitation::STATUS_OPENED], true)) {
|
||||
@@ -273,9 +274,86 @@ class HomeController extends AbstractController
|
||||
$invitation->setRespondedAt(new \DateTimeImmutable());
|
||||
$em->flush();
|
||||
|
||||
if ('accept' === $action) {
|
||||
$registerUrl = $this->generateUrl('app_invitation_register', [
|
||||
'token' => $invitation->getToken(),
|
||||
], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$html = $this->renderView('email/organizer_accepted.html.twig', [
|
||||
'invitation' => $invitation,
|
||||
'registerUrl' => $registerUrl,
|
||||
]);
|
||||
|
||||
$mailerService->sendEmail(
|
||||
$invitation->getEmail(),
|
||||
'Bienvenue sur E-Ticket - Finalisez votre inscription',
|
||||
$html,
|
||||
'E-Ticket <contact@e-cosplay.fr>',
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('home/invitation_response.html.twig', [
|
||||
'invitation' => $invitation,
|
||||
'accepted' => 'accept' === $action,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/invitation/{token}/inscription', name: 'app_invitation_register', methods: ['GET', 'POST'])]
|
||||
public function invitationRegister(string $token, Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordHasher): Response
|
||||
{
|
||||
$invitation = $em->getRepository(OrganizerInvitation::class)->findOneBy(['token' => $token]);
|
||||
if (!$invitation || OrganizerInvitation::STATUS_ACCEPTED !== $invitation->getStatus()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$existing = $em->getRepository(User::class)->findOneBy(['email' => $invitation->getEmail()]);
|
||||
if ($existing) {
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$firstName = trim($request->request->getString('first_name'));
|
||||
$lastName = trim($request->request->getString('last_name'));
|
||||
$password = $request->request->getString('password');
|
||||
$siret = trim($request->request->getString('siret'));
|
||||
$address = trim($request->request->getString('address'));
|
||||
$postalCode = trim($request->request->getString('postal_code'));
|
||||
$city = trim($request->request->getString('city'));
|
||||
$phone = trim($request->request->getString('phone'));
|
||||
|
||||
if ('' === $firstName || '' === $lastName || '' === $password) {
|
||||
$this->addFlash('error', 'Tous les champs obligatoires doivent etre remplis.');
|
||||
|
||||
return $this->redirectToRoute('app_invitation_register', ['token' => $token]);
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($invitation->getEmail());
|
||||
$user->setFirstName($firstName);
|
||||
$user->setLastName($lastName);
|
||||
$user->setPassword($passwordHasher->hashPassword($user, $password));
|
||||
$user->setRoles(['ROLE_ORGANIZER']);
|
||||
$user->setCompanyName($invitation->getCompanyName());
|
||||
$user->setOffer($invitation->getOffer());
|
||||
$user->setCommissionRate($invitation->getCommissionRate());
|
||||
$user->setIsApproved(true);
|
||||
$user->setIsVerified(true);
|
||||
$user->setSiret($siret ?: null);
|
||||
$user->setAddress($address ?: null);
|
||||
$user->setPostalCode($postalCode ?: null);
|
||||
$user->setCity($city ?: null);
|
||||
$user->setPhone($phone ?: null);
|
||||
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Votre compte organisateur a ete cree. Connectez-vous pour commencer.');
|
||||
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
return $this->render('home/invitation_register.html.twig', [
|
||||
'invitation' => $invitation,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
31
templates/email/organizer_accepted.html.twig
Normal file
31
templates/email/organizer_accepted.html.twig
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends 'email/base.html.twig' %}
|
||||
|
||||
{% block title %}Bienvenue sur E-Ticket{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Bienvenue sur E-Ticket !</h2>
|
||||
<p>Bonjour {{ invitation.firstName }},</p>
|
||||
<p>Merci d'avoir accepte notre invitation ! Votre compte organisateur pour <strong>{{ invitation.companyName }}</strong> est pre-approuve et pret a etre finalise.</p>
|
||||
|
||||
<div style="padding: 16px; background: #fabf04; border: 3px solid #111827; margin: 20px 0;">
|
||||
<p style="margin: 0; font-size: 13px; font-weight: 900; color: #111827;">
|
||||
Offre : {% if invitation.offer == 'free' %}Gratuit{% elseif invitation.offer == 'basic' %}Basic{% elseif invitation.offer == 'custom' %}Sur-mesure{% else %}{{ invitation.offer }}{% endif %}
|
||||
{% if invitation.commissionRate is not null %} — Commission : {{ invitation.commissionRate }}%{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Il ne vous reste plus qu'a finaliser votre inscription en choisissant un mot de passe et en completant vos informations.</p>
|
||||
|
||||
<p>Les informations suivantes sont deja pre-remplies :</p>
|
||||
<ul style="margin: 16px 0; padding-left: 20px;">
|
||||
<li style="margin-bottom: 4px; font-weight: 700;">Raison sociale : {{ invitation.companyName }}</li>
|
||||
<li style="margin-bottom: 4px; font-weight: 700;">Email : {{ invitation.email }}</li>
|
||||
<li style="margin-bottom: 4px; font-weight: 700;">Compte pre-approuve par E-Ticket</li>
|
||||
</ul>
|
||||
|
||||
<p style="text-align: center; margin: 28px 0;">
|
||||
<a href="{{ registerUrl }}" class="btn">Finaliser mon inscription</a>
|
||||
</p>
|
||||
|
||||
<p style="font-size: 12px; color: #9ca3af;">Ce lien est personnel et unique. Ne le partagez pas.</p>
|
||||
{% endblock %}
|
||||
86
templates/home/invitation_register.html.twig
Normal file
86
templates/home/invitation_register.html.twig
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Finaliser mon inscription - E-Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
|
||||
<section class="py-12 px-4">
|
||||
<div class="max-w-xl mx-auto">
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-block px-4 py-1 border-3 border-gray-900 bg-[#fabf04] font-black uppercase text-xs tracking-widest mb-4 shadow-[4px_4px_0px_rgba(0,0,0,1)]">Compte pre-approuve</div>
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page">Finaliser votre inscription</h1>
|
||||
<p class="font-bold text-gray-600 mt-2">{{ invitation.companyName }}</p>
|
||||
</div>
|
||||
|
||||
{% for message in app.flashes('error') %}
|
||||
<div class="flash-error"><p class="font-black text-sm">{{ message }}</p></div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="border-4 border-gray-900 bg-white p-6 shadow-[6px_6px_0px_rgba(0,0,0,1)]">
|
||||
<form method="post" action="{{ path('app_invitation_register', {token: invitation.token}) }}" class="space-y-4">
|
||||
|
||||
<div class="border-2 border-gray-200 bg-gray-50 p-4">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2">Informations pre-remplies</p>
|
||||
<div class="space-y-1 text-sm font-bold text-gray-600">
|
||||
<p>Raison sociale : <span class="text-gray-900">{{ invitation.companyName }}</span></p>
|
||||
<p>Email : <span class="text-gray-900">{{ invitation.email }}</span></p>
|
||||
<p>Offre : <span class="text-indigo-600">{% if invitation.offer == 'free' %}Gratuit{% elseif invitation.offer == 'basic' %}Basic{% elseif invitation.offer == 'custom' %}Sur-mesure{% endif %}</span>
|
||||
{% if invitation.commissionRate is not null %} — Commission : <span class="text-indigo-600">{{ invitation.commissionRate }}%</span>{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="reg_last_name" class="text-xs font-black uppercase tracking-widest form-label">Nom *</label>
|
||||
<input type="text" id="reg_last_name" name="last_name" required class="form-input focus:border-indigo-600" value="{{ invitation.lastName }}" placeholder="Dupont">
|
||||
</div>
|
||||
<div>
|
||||
<label for="reg_first_name" class="text-xs font-black uppercase tracking-widest form-label">Prenom *</label>
|
||||
<input type="text" id="reg_first_name" name="first_name" required class="form-input focus:border-indigo-600" value="{{ invitation.firstName }}" placeholder="Jean">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="reg_password" class="text-xs font-black uppercase tracking-widest form-label">Mot de passe *</label>
|
||||
<input type="password" id="reg_password" name="password" required minlength="8" class="form-input focus:border-indigo-600" placeholder="Minimum 8 caracteres">
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-200">
|
||||
|
||||
<div>
|
||||
<label for="reg_siret" class="text-xs font-black uppercase tracking-widest form-label">SIRET</label>
|
||||
<input type="text" id="reg_siret" name="siret" maxlength="14" class="form-input focus:border-indigo-600" placeholder="12345678901234">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="reg_address" class="text-xs font-black uppercase tracking-widest form-label">Adresse</label>
|
||||
<input type="text" id="reg_address" name="address" class="form-input focus:border-indigo-600" placeholder="42 rue de Saint-Quentin">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="reg_postal_code" class="text-xs font-black uppercase tracking-widest form-label">Code postal</label>
|
||||
<input type="text" id="reg_postal_code" name="postal_code" maxlength="10" class="form-input focus:border-indigo-600" placeholder="02800">
|
||||
</div>
|
||||
<div>
|
||||
<label for="reg_city" class="text-xs font-black uppercase tracking-widest form-label">Ville</label>
|
||||
<input type="text" id="reg_city" name="city" class="form-input focus:border-indigo-600" placeholder="Beautor">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="reg_phone" class="text-xs font-black uppercase tracking-widest form-label">Telephone</label>
|
||||
<input type="tel" id="reg_phone" name="phone" class="form-input focus:border-indigo-600" placeholder="06 00 00 00 00">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full px-6 py-4 border-4 border-gray-900 bg-[#fabf04] font-black uppercase text-sm tracking-widest shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:translate-y-[-2px] transition-all cursor-pointer">
|
||||
Creer mon compte organisateur
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-xs font-bold text-gray-400 text-center mt-4">Votre compte sera directement actif, sans validation supplementaire.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -10,10 +10,15 @@
|
||||
<div class="text-6xl mb-4 text-green-600">✓</div>
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page mb-4">Invitation acceptee</h1>
|
||||
<p class="font-bold text-gray-600 mb-2">Merci {{ invitation.firstName }} !</p>
|
||||
<p class="text-sm font-bold text-gray-500 mb-6">Votre compte organisateur pour <strong>{{ invitation.companyName }}</strong> sera bientot active. Vous recevrez un email de confirmation.</p>
|
||||
<a href="{{ path('app_home') }}" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
|
||||
Decouvrir E-Ticket
|
||||
</a>
|
||||
<p class="text-sm font-bold text-gray-500 mb-6">Un email avec un lien pour finaliser votre inscription a ete envoye a <strong>{{ invitation.email }}</strong>.</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<a href="{{ path('app_invitation_register', {token: invitation.token}) }}" class="btn-brutal font-black uppercase text-sm tracking-widest bg-[#fabf04] hover:bg-yellow-500 transition-all">
|
||||
Finaliser mon inscription
|
||||
</a>
|
||||
<a href="{{ path('app_home') }}" class="text-sm font-bold text-gray-500 hover:text-gray-900 transition-colors">
|
||||
Retour a l'accueil
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-6xl mb-4 text-gray-400">✕</div>
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page mb-4">Invitation refusee</h1>
|
||||
|
||||
Reference in New Issue
Block a user