feat: Implement password reset and change functionality

Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
Cursor Agent
2025-12-20 23:13:51 +00:00
co-authored by mathieumoran1
parent 7a5ef7e9a0
commit c83b4685a2
11 changed files with 386 additions and 18 deletions
+45
View File
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pool Oscars - Changer le mot de passe</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container d-flex justify-content-center align-items-center vh-100">
<div class="card p-4" style="max-width: 420px; width: 100%;">
<div class="text-center mb-3">
<img src="img/logo.png" style="width: 80%;">
</div>
<div class="alert alert-warning" role="alert">
Votre compte utilise un <strong>mot de passe temporaire</strong>. Veuillez définir un nouveau mot de passe pour continuer.
</div>
<div id="change-password-error" class="alert alert-danger d-none" role="alert"></div>
<form id="change-password-form">
<div class="mb-3">
<label for="new-password" class="form-label">Nouveau mot de passe</label>
<input type="password" class="form-control" id="new-password" minlength="8" required>
<div class="form-text">Minimum 8 caractères.</div>
</div>
<div class="mb-3">
<label for="confirm-password" class="form-label">Confirmer le mot de passe</label>
<input type="password" class="form-control" id="confirm-password" minlength="8" required>
</div>
<button type="submit" class="btn btn-primary w-100">Enregistrer</button>
</form>
<button type="button" id="log-off" class="btn btn-outline-danger w-100 mt-3">Déconnexion</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="./js/app_change-password.js"></script>
</body>
</html>
+24
View File
@@ -70,6 +70,30 @@
</div>
</div>
<h2>Admin - Réinitialiser mot de passe utilisateur</h2>
<p class="text-muted mb-3">
Génère un mot de passe temporaire aléatoire. L'utilisateur devra choisir un nouveau mot de passe à sa prochaine connexion.
</p>
<div id="user-reset-response" class="alert d-none" role="alert"></div>
<form id="reset-user-password-form" class="card p-3 mb-4">
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label for="reset_user_email" class="form-label">Email utilisateur</label>
<input type="email" class="form-control" id="reset_user_email" placeholder="[email protected]" required>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-danger w-100">Reset</button>
</div>
<div class="col-12">
<div class="form-text">
Important: le mot de passe temporaire est affiché une seule fois ici. Copie-le et envoie-le à l'utilisateur.
</div>
</div>
</div>
</form>
<h2>Admin - Ajouter un film</h2>
<p class="text-muted mb-4">
Ajoute un film via l’API (<code>/api/movies/add</code>). Le serveur récupère automatiquement le titre/plot/affiche depuis OMDb avec l’<code>imdb_id</code>.
+103
View File
@@ -0,0 +1,103 @@
function decodeJwt(token) {
return JSON.parse(atob(token.split('.')[1]));
}
function isTokenExpired(decoded) {
const currentTime = Math.floor(Date.now() / 1000);
return !!decoded.exp && decoded.exp < currentTime;
}
window.onload = function () {
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/';
return;
}
let decoded;
try {
decoded = decodeJwt(token);
} catch (e) {
localStorage.removeItem('auth_token');
window.location.href = '/';
return;
}
if (isTokenExpired(decoded)) {
localStorage.removeItem('auth_token');
window.location.href = '/';
return;
}
// Only show this page if user must change password.
if (!decoded.mustChangePassword) {
window.location.href = '/movies.html';
return;
}
const errorEl = document.getElementById('change-password-error');
const form = document.getElementById('change-password-form');
function showError(message) {
errorEl.classList.remove('d-none');
errorEl.textContent = message;
}
function clearError() {
errorEl.classList.add('d-none');
errorEl.textContent = '';
}
document.getElementById('log-off').addEventListener('click', function () {
localStorage.removeItem('auth_token');
window.location.href = '/';
});
form.addEventListener('submit', async function (e) {
e.preventDefault();
clearError();
const newPassword = document.getElementById('new-password').value;
const confirmPassword = document.getElementById('confirm-password').value;
if (newPassword.length < 8) {
showError('Le mot de passe doit contenir au moins 8 caractères.');
return;
}
if (newPassword !== confirmPassword) {
showError('Les mots de passe ne correspondent pas.');
return;
}
try {
const res = await fetch('/api/users/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ newPassword }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
showError(data.message || data.error || `Erreur (${res.status})`);
return;
}
if (data.token) {
localStorage.setItem('auth_token', data.token);
} else {
// Fallback: force re-login if no token is returned.
localStorage.removeItem('auth_token');
window.location.href = '/';
return;
}
window.location.href = '/movies.html';
} catch (err) {
showError(err.message || 'Erreur réseau');
}
});
};
+4
View File
@@ -27,6 +27,10 @@ window.onload = async function () {
if (token) {
try {
const decoded = JSON.parse(atob(token.split('.')[1]));
if (decoded.mustChangePassword) {
window.location.href = '/change-password.html';
return;
}
const userName = decoded.name;
document.getElementById('user-name').textContent = userName;
if (decoded.admin) {
+51
View File
@@ -21,6 +21,11 @@ window.onload = async function () {
return;
}
if (decoded.mustChangePassword) {
window.location.href = '/change-password.html';
return;
}
document.getElementById('user-name').textContent = decoded.name || '';
// Admin-only page: if token doesn't contain admin flag, bounce.
@@ -68,6 +73,17 @@ window.onload = async function () {
responseEl.textContent = message;
}
const userResetResponseEl = document.getElementById('user-reset-response');
const userResetForm = document.getElementById('reset-user-password-form');
const userResetEmailEl = document.getElementById('reset_user_email');
function showUserResetResponse(kind, message) {
if (!userResetResponseEl) return;
userResetResponseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
userResetResponseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
userResetResponseEl.textContent = message;
}
function parseYear(value) {
const n = Number(value);
if (!Number.isInteger(n)) return null;
@@ -123,6 +139,41 @@ window.onload = async function () {
window.location.href = '/';
});
if (userResetForm && userResetEmailEl) {
userResetForm.addEventListener('submit', async function (e) {
e.preventDefault();
const email = (userResetEmailEl.value || '').trim();
if (!email) {
showUserResetResponse('warning', 'Email invalide.');
return;
}
try {
showUserResetResponse('warning', 'Génération du mot de passe temporaire...');
const res = await fetch('/api/users/admin/reset-temp-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ email }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
showUserResetResponse('danger', data.message || data.error || `Erreur (${res.status})`);
return;
}
const label = data?.user?.name ? `${data.user.name} (${data.user.email})` : email;
const expiresAt = data?.expiresAt ? ` Expire: ${data.expiresAt}` : '';
showUserResetResponse('success', `Temp password pour ${label}: ${data.tempPassword}.${expiresAt}`);
} catch (err) {
showUserResetResponse('danger', err.message || 'Erreur réseau');
}
});
}
form.addEventListener('submit', async function (e) {
e.preventDefault();
+25 -4
View File
@@ -1,8 +1,24 @@
function decodeJwt(token) {
return JSON.parse(atob(token.split('.')[1]));
}
// Check if the user is already logged in by looking for auth_token in localStorage
if (localStorage.getItem('auth_token')) {
// If the token is found, redirect to /movies.html
window.location.href = '/movies.html';
const existingToken = localStorage.getItem('auth_token');
if (existingToken) {
try {
const decoded = decodeJwt(existingToken);
const currentTime = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp < currentTime) {
localStorage.removeItem('auth_token');
} else if (decoded.mustChangePassword) {
window.location.href = '/change-password.html';
} else {
window.location.href = '/movies.html';
}
} catch (_) {
localStorage.removeItem('auth_token');
}
}
// Handle login form submission
document.getElementById('login-form').addEventListener('submit', async function (e) {
@@ -21,7 +37,12 @@ if (localStorage.getItem('auth_token')) {
const data = await res.json();
// Save the token to localStorage
localStorage.setItem('auth_token', data.token);
window.location.href = '/movies.html'; // Redirect to movies page on success
if (data.mustChangePassword) {
window.location.href = '/change-password.html';
} else {
window.location.href = '/movies.html'; // Redirect to movies page on success
}
} else {
const error = await res.json();
alert(`${error.message}`);
+4
View File
@@ -50,6 +50,10 @@ window.onload = async function () {
if (token) {
try {
const decoded = JSON.parse(atob(token.split('.')[1]));
if (decoded.mustChangePassword) {
window.location.href = '/change-password.html';
return;
}
const userName = decoded.name;
document.getElementById('user-name').textContent = userName;
if (decoded.admin) {
+19 -3
View File
@@ -1,8 +1,24 @@
function decodeJwt(token) {
return JSON.parse(atob(token.split('.')[1]));
}
// Check if the user is already logged in by looking for auth_token in localStorage
if (localStorage.getItem('auth_token')) {
// If the token is found, redirect to /movies.html
window.location.href = '/movies.html';
const existingToken = localStorage.getItem('auth_token');
if (existingToken) {
try {
const decoded = decodeJwt(existingToken);
const currentTime = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp < currentTime) {
localStorage.removeItem('auth_token');
} else if (decoded.mustChangePassword) {
window.location.href = '/change-password.html';
} else {
window.location.href = '/movies.html';
}
} catch (_) {
localStorage.removeItem('auth_token');
}
}
// Handle registration form submission
document.getElementById('register-form').addEventListener('submit', async function (e) {
+4
View File
@@ -16,6 +16,10 @@ window.onload = async function () {
if (token) {
try {
const decoded = JSON.parse(atob(token.split('.')[1])); // Manually decoding JWT
if (decoded.mustChangePassword) {
window.location.href = '/change-password.html';
return;
}
const userName = decoded.name;
document.getElementById('user-name').textContent = userName;
if (decoded.admin) {