Version initiale — carte des établissements scolaires
Carte Leaflet (annuaire Éducation nationale) + API PHP/MySQL : fiche établissement (effectifs, IPS, population INSEE, DPE ADEME), GED documents/photos, commentaires par groupe, gestion utilisateurs et politique de mots de passe. Imports opendata (effectifs, IPS, population, DPE, IRIS). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* Authentification et gestion du mot de passe.
|
||||
* GET ?action=me → utilisateur courant (ou null)
|
||||
* POST ?action=login → {username, password}
|
||||
* POST ?action=logout
|
||||
* POST ?action=change_password → {current, new} (utilisateur connecté)
|
||||
* POST ?action=forgot → {login} (identifiant ou e-mail)
|
||||
* POST ?action=reset → {token, new} (lien de réinitialisation)
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/db.php';
|
||||
|
||||
const LOGIN_MAX_ATTEMPTS = 5; // échecs avant verrouillage
|
||||
const LOGIN_LOCK_MINUTES = 15; // durée du verrou
|
||||
const RESET_TTL_MINUTES = 60; // validité d'un lien de réinitialisation
|
||||
|
||||
try {
|
||||
$action = $_GET['action'] ?? 'me';
|
||||
|
||||
if ($action === 'me') {
|
||||
json_out(['user' => current_user()]);
|
||||
}
|
||||
|
||||
// ---- Connexion --------------------------------------------------------
|
||||
if ($action === 'login') {
|
||||
$b = body_json();
|
||||
$username = trim((string) ($b['username'] ?? ''));
|
||||
$password = (string) ($b['password'] ?? '');
|
||||
if ($username === '' || $password === '') {
|
||||
json_error('Identifiant et mot de passe requis.');
|
||||
}
|
||||
$st = db()->prepare('SELECT id, username, password_hash, role, must_change, failed_attempts, locked_until FROM users WHERE username = ?');
|
||||
$st->execute([$username]);
|
||||
$u = $st->fetch();
|
||||
|
||||
// Verrou anti-bruteforce.
|
||||
if ($u && $u['locked_until'] !== null && strtotime($u['locked_until']) > time()) {
|
||||
json_error('Compte temporairement verrouillé suite à trop d’essais. Réessayez plus tard.', 429);
|
||||
}
|
||||
|
||||
if (!$u || !password_verify($password, $u['password_hash'])) {
|
||||
if ($u) {
|
||||
$attempts = (int) $u['failed_attempts'] + 1;
|
||||
if ($attempts >= LOGIN_MAX_ATTEMPTS) {
|
||||
db()->prepare('UPDATE users SET failed_attempts = 0, locked_until = DATE_ADD(NOW(), INTERVAL ? MINUTE) WHERE id = ?')
|
||||
->execute([LOGIN_LOCK_MINUTES, (int) $u['id']]);
|
||||
} else {
|
||||
db()->prepare('UPDATE users SET failed_attempts = ? WHERE id = ?')
|
||||
->execute([$attempts, (int) $u['id']]);
|
||||
}
|
||||
}
|
||||
json_error('Identifiants invalides.', 401);
|
||||
}
|
||||
|
||||
// Succès : réinitialise les compteurs et régénère la session.
|
||||
db()->prepare('UPDATE users SET failed_attempts = 0, locked_until = NULL WHERE id = ?')
|
||||
->execute([(int) $u['id']]);
|
||||
session_boot();
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['uid'] = (int) $u['id'];
|
||||
json_out(['user' => [
|
||||
'id' => (int) $u['id'],
|
||||
'username' => $u['username'],
|
||||
'role' => $u['role'],
|
||||
'must_change' => (int) $u['must_change'],
|
||||
]]);
|
||||
}
|
||||
|
||||
if ($action === 'logout') {
|
||||
session_boot();
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
json_out(['ok' => true]);
|
||||
}
|
||||
|
||||
// ---- Changement de mot de passe (connecté) ----------------------------
|
||||
if ($action === 'change_password') {
|
||||
$me = require_login();
|
||||
$b = body_json();
|
||||
$current = (string) ($b['current'] ?? '');
|
||||
$new = (string) ($b['new'] ?? '');
|
||||
|
||||
$st = db()->prepare('SELECT password_hash FROM users WHERE id = ?');
|
||||
$st->execute([(int) $me['id']]);
|
||||
$hash = $st->fetchColumn();
|
||||
if (!$hash || !password_verify($current, $hash)) {
|
||||
json_error('Mot de passe actuel incorrect.', 403);
|
||||
}
|
||||
if ($new === $current) {
|
||||
json_error('Le nouveau mot de passe doit être différent de l’actuel.');
|
||||
}
|
||||
if ($err = password_policy_check($new)) {
|
||||
json_error($err);
|
||||
}
|
||||
db()->prepare('UPDATE users SET password_hash = ?, must_change = 0 WHERE id = ?')
|
||||
->execute([password_hash($new, PASSWORD_DEFAULT), (int) $me['id']]);
|
||||
json_out(['ok' => true]);
|
||||
}
|
||||
|
||||
// ---- Mot de passe oublié : demande de lien ----------------------------
|
||||
if ($action === 'forgot') {
|
||||
$login = trim((string) (body_json()['login'] ?? ''));
|
||||
// Réponse générique systématique (ne révèle pas l'existence d'un compte).
|
||||
$generic = ['ok' => true, 'message' => 'Si un compte correspond, un e-mail de réinitialisation a été envoyé.'];
|
||||
if ($login === '') {
|
||||
json_out($generic);
|
||||
}
|
||||
$st = db()->prepare('SELECT id, username, email FROM users WHERE username = ? OR (email IS NOT NULL AND email = ?) LIMIT 1');
|
||||
$st->execute([$login, $login]);
|
||||
$u = $st->fetch();
|
||||
if ($u && !empty($u['email'])) {
|
||||
// Invalide les anciens jetons non utilisés puis en crée un nouveau.
|
||||
db()->prepare('UPDATE password_resets SET used = 1 WHERE user_id = ? AND used = 0')->execute([(int) $u['id']]);
|
||||
$token = reset_token_new();
|
||||
db()->prepare('INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? MINUTE))')
|
||||
->execute([(int) $u['id'], hash('sha256', $token), RESET_TTL_MINUTES]);
|
||||
$link = APP_BASE_URL . '/reset.html?token=' . $token;
|
||||
$body = "Bonjour,\n\nUne réinitialisation du mot de passe du compte « {$u['username']} » a été demandée.\n"
|
||||
. "Cliquez sur ce lien (valable " . RESET_TTL_MINUTES . " minutes) pour choisir un nouveau mot de passe :\n\n$link\n\n"
|
||||
. "Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail.\n";
|
||||
send_mail($u['email'], 'Réinitialisation de votre mot de passe', $body);
|
||||
}
|
||||
json_out($generic);
|
||||
}
|
||||
|
||||
// ---- Mot de passe oublié : application du nouveau mot de passe ---------
|
||||
if ($action === 'reset') {
|
||||
$b = body_json();
|
||||
$token = (string) ($b['token'] ?? '');
|
||||
$new = (string) ($b['new'] ?? '');
|
||||
if ($token === '') {
|
||||
json_error('Lien invalide.');
|
||||
}
|
||||
$st = db()->prepare('SELECT id, user_id FROM password_resets WHERE token_hash = ? AND used = 0 AND expires_at > NOW() LIMIT 1');
|
||||
$st->execute([hash('sha256', $token)]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) {
|
||||
json_error('Lien de réinitialisation invalide ou expiré.', 400);
|
||||
}
|
||||
if ($err = password_policy_check($new)) {
|
||||
json_error($err);
|
||||
}
|
||||
db()->prepare('UPDATE users SET password_hash = ?, must_change = 0, failed_attempts = 0, locked_until = NULL WHERE id = ?')
|
||||
->execute([password_hash($new, PASSWORD_DEFAULT), (int) $row['user_id']]);
|
||||
db()->prepare('UPDATE password_resets SET used = 1 WHERE id = ?')->execute([(int) $row['id']]);
|
||||
json_out(['ok' => true]);
|
||||
}
|
||||
|
||||
json_error('Action inconnue.', 404);
|
||||
} catch (Throwable $e) {
|
||||
json_error('Erreur : ' . $e->getMessage(), 500);
|
||||
}
|
||||
Reference in New Issue
Block a user