Skip to content

Commit bc44bfa

Browse files
committed
Refacto and UI imrpovement
1 parent 5192d49 commit bc44bfa

9 files changed

Lines changed: 354 additions & 9 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Ajoute le champ de vérification d'email pour l'inscription
2+
ALTER TABLE {prefix}users ADD COLUMN email_verified TINYINT(1) NOT NULL DEFAULT 1 AFTER password;
3+

public/index.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,13 @@ function (\Psr\Http\Message\ServerRequestInterface $request, \Throwable $excepti
190190
true // handleSubclasses
191191
);
192192
$app->get('/', [HomeController::class, 'index'])->setName('app_index');
193+
$app->get('/register', [HomeController::class, 'register'])->setName('app_register');
193194
$app->get('/forgot_password', [HomeController::class, 'forgotPassword'])->setName('app_forgot_password');
194195
$app->get('/reset_password/{token}', [HomeController::class, 'resetPassword'])->setName('app_reset_password');
195196

196197
$app->post('/login', [LoginController::class, 'login'])->setName('app_login');
198+
$app->post('/register', [LoginController::class, 'register'])->setName('app_register_post');
199+
$app->get('/verify-email/{token}', [LoginController::class, 'verifyEmail'])->setName('app_verify_email');
197200
$app->get('/logout', [LoginController::class, 'logout'])->add(new AuthMiddleware())->setName('app_logout');
198201
$app->post('/forgot_password', [LoginController::class, 'forgotPassword'])->setName('app_forgot_password_post');
199202
$app->post('/reset_password', [LoginController::class, 'resetPassword'])->setName('app_reset_password_post');

src/Controllers/HomeController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ public function index(Request $request, Response $response): Response
2020
return $this->view->render($response, 'index.html.twig', $messages);
2121
}
2222

23+
public function register(Request $request, Response $response): Response
24+
{
25+
$messages = $this->messages->getMessages();
26+
return $this->view->render($response, 'register.html.twig', $messages);
27+
}
28+
2329
public function forgotPassword(Request $request, Response $response): Response
2430
{
2531
return $this->view->render($response, 'password-forgot.html.twig');

src/Controllers/LoginController.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ private function redirectToRoute(Request $request, Response $response, string $r
3939

4040
/**
4141
* Valide la page de connexion après soumission du formulaire.
42+
* Vérifie aussi que l'email est confirmé.
4243
*/
4344
public function login(Request $request, Response $response): Response
4445
{
@@ -48,6 +49,10 @@ public function login(Request $request, Response $response): Response
4849
$user = $this->userRepository->select($email);
4950

5051
if ($user && password_verify($password, $user->password)) {
52+
if (isset($user->email_verified) && !$user->email_verified) {
53+
$this->messages->addMessage('email_not_verified', true);
54+
return $this->redirectToRoute($request, $response, 'app_index');
55+
}
5156
session_regenerate_id(true);
5257
$_SESSION['user'] = $user;
5358
return $this->redirectToRoute($request, $response, 'app_admin_index');
@@ -57,6 +62,128 @@ public function login(Request $request, Response $response): Response
5762
return $this->redirectToRoute($request, $response, 'app_index');
5863
}
5964

65+
/**
66+
* Inscrit un nouvel utilisateur avec vérification par email.
67+
*/
68+
public function register(Request $request, Response $response): Response
69+
{
70+
$data = $request->getParsedBody();
71+
$email = trim($data['email'] ?? '');
72+
$password = $data['password'] ?? '';
73+
$passwordRepeat = $data['passwordRepeat'] ?? '';
74+
75+
// Validation email
76+
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
77+
$this->messages->addMessage('register_error', 'Adresse email invalide.');
78+
return $this->redirectToRoute($request, $response, 'app_register');
79+
}
80+
81+
// Validation mot de passe
82+
$passwordErrors = $this->validatePassword($password, $passwordRepeat);
83+
if (!empty($passwordErrors)) {
84+
$this->messages->addMessage('register_error', implode(' ', $passwordErrors));
85+
return $this->redirectToRoute($request, $response, 'app_register');
86+
}
87+
88+
// Vérifier si l'utilisateur existe déjà
89+
$existing = $this->userRepository->select($email);
90+
if ($existing) {
91+
$this->messages->addMessage('register_error', 'Un compte avec cet email existe déjà.');
92+
return $this->redirectToRoute($request, $response, 'app_register');
93+
}
94+
95+
// Créer l'utilisateur (email non vérifié)
96+
$user = $this->userRepository->insertWithPassword($email, $password);
97+
$user = $this->userRepository->insertResetToken($user);
98+
99+
// Envoyer l'email de vérification
100+
$routeParser = RouteContext::fromRequest($request)->getRouteParser();
101+
$verifyUrl = $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_verify_email', ["token" => $user->reset_token]);
102+
103+
$this->mailchimp->messages->send([
104+
"message" => [
105+
"from_email" => "contact@helloasso.io",
106+
"from_name" => "HelloAsso",
107+
"subject" => "Confirmez votre adresse email",
108+
"html" => $this->buildVerificationEmail($verifyUrl),
109+
"to" => [["email" => $user->email]],
110+
],
111+
]);
112+
113+
$this->messages->addMessage('register_success', true);
114+
return $this->redirectToRoute($request, $response, 'app_index');
115+
}
116+
117+
/**
118+
* Vérifie l'email via le token reçu par mail.
119+
*/
120+
public function verifyEmail(Request $request, Response $response, array $args): Response
121+
{
122+
$token = $args['token'] ?? '';
123+
$user = $this->userRepository->selectByToken($token);
124+
125+
if ($user) {
126+
$this->userRepository->verifyEmail($user);
127+
$this->messages->addMessage('email_verified', true);
128+
} else {
129+
$this->messages->addMessage('email_verify_error', true);
130+
}
131+
132+
return $this->redirectToRoute($request, $response, 'app_index');
133+
}
134+
135+
/**
136+
* Valide les règles de mot de passe.
137+
*/
138+
private function validatePassword(string $password, string $passwordRepeat): array
139+
{
140+
$errors = [];
141+
142+
if ($password !== $passwordRepeat) {
143+
$errors[] = 'Les mots de passe ne correspondent pas.';
144+
}
145+
if (strlen($password) < 8) {
146+
$errors[] = 'Le mot de passe doit contenir au moins 8 caractères.';
147+
}
148+
if (!preg_match('/[A-Z]/', $password)) {
149+
$errors[] = 'Le mot de passe doit contenir au moins une majuscule.';
150+
}
151+
if (!preg_match('/[a-z]/', $password)) {
152+
$errors[] = 'Le mot de passe doit contenir au moins une minuscule.';
153+
}
154+
if (!preg_match('/[0-9]/', $password)) {
155+
$errors[] = 'Le mot de passe doit contenir au moins un chiffre.';
156+
}
157+
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
158+
$errors[] = 'Le mot de passe doit contenir au moins un caractère spécial.';
159+
}
160+
161+
return $errors;
162+
}
163+
164+
/**
165+
* Génère le contenu HTML de l'email de vérification.
166+
*/
167+
private function buildVerificationEmail(string $verifyUrl): string
168+
{
169+
return <<<HTML
170+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;">
171+
<h1 style="color: #2C88D9;">Confirmez votre adresse email 📧</h1>
172+
<p>Bonjour,</p>
173+
<p>Merci de vous être inscrit sur <strong>HelloAsso Stream</strong> !</p>
174+
<p>Pour activer votre compte, veuillez confirmer votre adresse email en cliquant sur le bouton ci-dessous :</p>
175+
<p style="text-align: center; margin: 30px 0;">
176+
<a href="{$verifyUrl}" style="background-color: #2C88D9; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Confirmer mon email</a>
177+
</p>
178+
<p style="font-size: 12px; color: #888;">Ou copiez ce lien dans votre navigateur : {$verifyUrl}</p>
179+
<p style="font-size: 12px; color: #888;">Ce lien est valable 1 heure.</p>
180+
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;" />
181+
<p>Si vous n'êtes pas à l'origine de cette inscription, vous pouvez ignorer cet email.</p>
182+
<p>L'équipe HelloAsso</p>
183+
</div>
184+
HTML;
185+
}
186+
60187
/**
61188
* Envoie un email de réinitialisation de mot de passe si l'adresse existe.
62189
*/

src/Models/User.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class User
77
public $id;
88
public $email;
99
public $password;
10+
public $email_verified;
1011
public $role;
1112
public $reset_token;
1213
public $reset_token_expires_at;

src/Repositories/UserRepository.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,37 @@ public function insert(string $email): User
3232
return $user;
3333
}
3434

35+
/**
36+
* Crée un utilisateur avec un mot de passe choisi et l'email non vérifié.
37+
*/
38+
public function insertWithPassword(string $email, string $password): User
39+
{
40+
$stmt = $this->pdo->prepare('INSERT INTO ' . $this->prefix . 'users (email, password, email_verified) VALUES (:email, :password, 0)');
41+
$stmt->execute([
42+
':email' => $email,
43+
':password' => password_hash($password, PASSWORD_DEFAULT),
44+
]);
45+
46+
$user = new User();
47+
$user->id = $this->pdo->lastInsertId();
48+
$user->email = $email;
49+
$user->email_verified = 0;
50+
51+
return $user;
52+
}
53+
54+
/**
55+
* Marque l'email d'un utilisateur comme vérifié.
56+
*/
57+
public function verifyEmail(User $user): void
58+
{
59+
$stmt = $this->pdo->prepare('UPDATE ' . $this->prefix . 'users SET email_verified = 1, reset_token = NULL, reset_token_expires_at = NULL WHERE id = :id');
60+
$stmt->execute([':id' => $user->id]);
61+
$user->email_verified = 1;
62+
$user->reset_token = null;
63+
$user->reset_token_expires_at = null;
64+
}
65+
3566
public function insertRight(User $user, ?Stream $stream, ?Event $event): void
3667
{
3768
$stmt = $this->pdo->prepare('INSERT INTO ' . $this->prefix . 'user_right (id_user, id_charity_event, id_charity_stream) VALUES (:id_user, :id_charity_event, :id_charity_stream)');

src/views/index.html.twig

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{% block title %}HelloAsso Widgets{% endblock %}
33
{% extends "layout.html.twig" %}
44
{% block content %}
5-
<div class="container">
5+
<div class="container" style="max-width: 480px;">
66
<h1 class="my-4 text-center">HelloAsso Widgets</h1>
77
<form action="/login" method="POST">
88
<div class="form-group">
@@ -16,7 +16,10 @@
1616
<div class="form-group">
1717
<a href="/forgot_password">Mot de passe oublié ?</a>
1818
</div>
19-
<button type="submit" class="btn btn-primary mt-3">Connexion 🚀</button>
19+
<button type="submit" class="btn btn-primary mt-3 w-100">Connexion 🚀</button>
20+
<div class="text-center mt-3">
21+
<a href="/register">Pas encore de compte ? Créer un compte</a>
22+
</div>
2023
</form>
2124
</div>
2225
{% if login_failed %}
@@ -60,6 +63,62 @@
6063
</div>
6164
</div>
6265
{% endif %}
66+
67+
{% if register_success %}
68+
<div class="toast-container position-fixed bottom-0 end-0 p-3">
69+
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
70+
<div class="toast-header">
71+
<strong class="me-auto">Inscription réussie</strong>
72+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
73+
</div>
74+
<div class="toast-body">
75+
Un email de vérification vient de vous être envoyé 📧 Consultez votre boîte mail pour activer votre compte.
76+
</div>
77+
</div>
78+
</div>
79+
{% endif %}
80+
81+
{% if email_verified %}
82+
<div class="toast-container position-fixed bottom-0 end-0 p-3">
83+
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
84+
<div class="toast-header">
85+
<strong class="me-auto">Email vérifié</strong>
86+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
87+
</div>
88+
<div class="toast-body">
89+
Votre email a été confirmé, vous pouvez vous connecter 🎉
90+
</div>
91+
</div>
92+
</div>
93+
{% endif %}
94+
95+
{% if email_verify_error %}
96+
<div class="toast-container position-fixed bottom-0 end-0 p-3">
97+
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
98+
<div class="toast-header">
99+
<strong class="me-auto">Erreur</strong>
100+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
101+
</div>
102+
<div class="toast-body">
103+
Le lien de vérification est invalide ou a expiré 😞
104+
</div>
105+
</div>
106+
</div>
107+
{% endif %}
108+
109+
{% if email_not_verified %}
110+
<div class="toast-container position-fixed bottom-0 end-0 p-3">
111+
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
112+
<div class="toast-header">
113+
<strong class="me-auto">Email non vérifié</strong>
114+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
115+
</div>
116+
<div class="toast-body">
117+
Veuillez confirmer votre adresse email avant de vous connecter. Consultez votre boîte mail 📧
118+
</div>
119+
</div>
120+
</div>
121+
{% endif %}
63122
{% endblock %}
64123
{% block scripts %}
65124
{% endblock %}

0 commit comments

Comments
 (0)