feat: Implement password reset and change functionality
Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
7a5ef7e9a0
commit
c83b4685a2
@@ -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>
|
||||
|
||||
@@ -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>.
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user