0) { header('Location: /login'); exit(); } $error = null; $checks = InstallationService::runtimeChecks(); $canInstall = InstallationService::canInstall($checks); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { if (!Auth::validCsrf($_POST['csrf'] ?? null)) { $error = t('auth.session_expired'); } elseif (!$canInstall) { $error = t('install.requirements_block'); } else { $username = trim((string) ($_POST['username'] ?? '')); $display = trim((string) ($_POST['display_name'] ?? '')); $password = (string) ($_POST['password'] ?? ''); $confirm = (string) ($_POST['password_confirm'] ?? ''); $association = trim((string) ($_POST['association_name'] ?? '')); $email = trim((string) ($_POST['association_email'] ?? '')); $language = (string) ($_POST['app_language'] ?? 'fr'); $routing = (string) ($_POST['public_site_routing'] ?? 'integrated'); if ($association === '') { $error = t('install.association_required'); } elseif ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { $error = t('settings.invalid_email'); } elseif (!preg_match('/^[a-zA-Z0-9._-]{3,40}$/', $username)) { $error = t('auth.invalid_username'); } elseif ($display === '') { $error = t('auth.display_required'); } elseif (strlen($password) < 10) { $error = t('auth.password_min', ['count' => 10]); } elseif ($password !== $confirm) { $error = t('auth.password_mismatch'); } else { $db = DB::pdo(); try { $db->beginTransaction(); $db->prepare( "INSERT INTO users(username,password_hash,display_name,role) VALUES(:u,:p,:d,'admin')", )->execute([ ':u' => $username, ':p' => password_hash($password, PASSWORD_DEFAULT), ':d' => $display, ]); AppSettings::save( [ 'association_name' => $association, 'association_address' => trim((string) ($_POST['association_address'] ?? '')), 'association_postal_code' => trim((string) ($_POST['association_postal_code'] ?? '')), 'association_city' => trim((string) ($_POST['association_city'] ?? '')), 'association_phone' => trim((string) ($_POST['association_phone'] ?? '')), 'association_email' => $email, 'association_siret' => trim((string) ($_POST['association_siret'] ?? '')), 'association_rna' => trim((string) ($_POST['association_rna'] ?? '')), 'app_language' => isset(I18n::LOCALES[$language]) ? $language : 'fr', 'public_site_routing' => in_array($routing, ['integrated', 'root'], true) ? $routing : 'integrated', ], (int) $db->lastInsertId(), ); $db->commit(); Auth::login($username, $password); AuditService::log( 'setup', '/setup', 'Installation initiale et création du premier administrateur', 'user', Auth::id(), ); header('Location: /settings/health?installed=1'); exit(); } catch (Throwable $e) { if ($db->inTransaction()) { $db->rollBack(); } $error = SecurityService::publicError($e); } } } } self::view(t('auth.first_setup'), 'setup', $error, [ 'checks' => $checks, 'canInstall' => $canInstall, 'publicPath' => InstallationService::publicPath(), ]); } public static function setupUnavailable(array $checks): void { self::view(t('auth.first_setup'), 'setup', $error = t('install.runtime_missing'), [ 'checks' => $checks, 'canInstall' => false, 'publicPath' => InstallationService::publicPath(), ]); } public static function login(): void { if (Auth::countUsers() === 0) { header('Location: /setup'); exit(); } if (Auth::user()) { header('Location: ' . PermissionService::firstAllowedPath()); exit(); } $error = null; if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { if (!Auth::validCsrf($_POST['csrf'] ?? null)) { $error = t('auth.session_expired'); } elseif ( Auth::login( (string) ($_POST['username'] ?? ''), (string) ($_POST['password'] ?? ''), isset($_POST['remember_device']), (string) ($_POST['device_name'] ?? ''), ) ) { AuditService::log('login', '/login', 'Connexion réussie'); $next = (string) ($_POST['next'] ?? ''); if ( !str_starts_with($next, '/') || str_starts_with($next, '//') || !PermissionService::canOpenPath((string) (parse_url($next, PHP_URL_PATH) ?: '/')) ) { $next = PermissionService::firstAllowedPath(); } header('Location: ' . $next); exit(); } else { AuditService::log('login_failed', '/login', 'Échec de connexion', null, null, [ 'username_hash' => hash('sha256', mb_strtolower(trim((string) ($_POST['username'] ?? '')))), ]); $error = t('auth.invalid_credentials'); } } self::view(t('auth.login'), 'login', $error); } public static function logout(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' || !Auth::validCsrf($_POST['csrf'] ?? null)) { http_response_code(400); return; } AuditService::log('logout', '/logout', 'Déconnexion'); Auth::logout(); header('Location: /login'); exit(); } private static function view(string $title, string $mode, ?string $error, array $vars = []): void { extract($vars, EXTR_SKIP); require __DIR__ . '/../Views/auth.php'; } }