Files
Arnaud 5633969d98 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>
2026-06-03 21:35:41 +02:00

154 lines
6.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 dessais. 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 lactuel.');
}
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);
}