diff --git a/config/auth.php b/config/auth.php index 5ff0f16..4a2cc27 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,24 +1,13 @@ 'shared'). +// Os nomes e assinaturas mantêm-se exactamente iguais porque há ~30 +// ficheiros em views/ a chamá-las; só o que está por baixo mudou. // ─── Role ladder ───────────────────────────────────────────────────────────── // sysadmin — super-admin (backups, platform update, app_base_url). @@ -29,6 +18,12 @@ function require_login(): array { // client — portal-only. // is_admin()/require_admin() = sysadmin OR admin. is_staff()/require_staff() // also includes the plain `staff` role. +// +// A hierarquia é agora expressa por posto numérico (auth_roles_register() em +// config/db.php: sysadmin=100, admin=80, staff=40, client=10) em vez dos +// antigos in_array() — auth_at_least('admin') dá exactamente o mesmo +// resultado que in_array($role, ['sysadmin','admin']) dava, porque staff(40) +// e client(10) ficam sempre abaixo de admin(80). // Resolves the dashboard path for a given role. Used by index.php, the auth // pages, and require_admin()'s bounce fallback so all four agree on routing. @@ -41,12 +36,27 @@ function post_login_destination(string $role): string { }; } +function require_login(): array { + // O «manter sessão iniciada» já corre em config/db.php + // (platform_connect(['remember' => true, ...]) chama auth_remember_resume() + // logo no arranque, antes de qualquer view), por isso não há aqui nada + // equivalente ao antigo remember_resume() manual — se havia um cookie + // válido, $_SESSION['user'] já está preenchido a esta altura. + if (!auth_check()) { + $return = $_SERVER['REQUEST_URI'] ?? ''; + $qs = $return ? ('?return_to=' . urlencode($return)) : ''; + header('Location: ' . base_url('views/auth/login.php' . $qs)); + exit; + } + return auth_user(); +} + // Accepts sysadmin + admin only. Plain staff and clients are bounced to the // dashboard appropriate for their role (avoids redirect loops when an admin- // only page bounces a staff user). function require_admin(): array { $user = require_login(); - if (!in_array($user['role'], ['sysadmin', 'admin'])) { + if (!auth_at_least('admin')) { header('Location: ' . base_url(post_login_destination($user['role'] ?? ''))); exit; } @@ -57,7 +67,7 @@ function require_admin(): array { // Clients are redirected to the public index. function require_staff(): array { $user = require_login(); - if (!in_array($user['role'], ['sysadmin', 'admin', 'staff'])) { + if (!auth_at_least('staff')) { header('Location: ' . base_url('index.php')); exit; } @@ -70,7 +80,7 @@ function require_staff(): array { // the role-appropriate sessions list. function require_session_ownership(array $session): array { $user = require_staff(); - if (in_array($user['role'], ['sysadmin', 'admin'], true)) return $user; + if (auth_at_least('admin')) return $user; if ((int) ($session['photographer_id'] ?? 0) !== (int) $user['id']) { header('Location: ' . base_url('views/sessions/index.php')); exit; @@ -82,7 +92,7 @@ function require_session_ownership(array $session): array { // Plain admins are sent back to their dashboard. function require_sysadmin(): array { $user = require_login(); - if ($user['role'] !== 'sysadmin') { + if (!auth_is('sysadmin')) { header('Location: ' . base_url(post_login_destination($user['role'] ?? ''))); exit; } @@ -90,34 +100,34 @@ function require_sysadmin(): array { } function current_user(): ?array { - return $_SESSION['user'] ?? null; + return auth_user(); } // True for sysadmin + admin (NOT plain staff). function is_admin(): bool { - return in_array(current_user()['role'] ?? '', ['sysadmin', 'admin']); + return auth_at_least('admin'); } // True for any backoffice user (sysadmin + admin + staff). Use this to show // a UI element that any non-client backoffice user should see. function is_staff(): bool { - return in_array(current_user()['role'] ?? '', ['sysadmin', 'admin', 'staff']); + return auth_at_least('staff'); } function is_sysadmin(): bool { - return (current_user()['role'] ?? '') === 'sysadmin'; + return auth_is('sysadmin'); } // True for plain admins (not sysadmin, not staff). Useful for "hide from // sysadmin" edge cases; most code wants is_admin() or is_sysadmin(). function is_admin_role(): bool { - return (current_user()['role'] ?? '') === 'admin'; + return auth_is('admin'); } // True for plain staff only — the assignment-only role. Used to scope queries // to "photographer_id = me" instead of returning all sessions. function is_staff_role(): bool { - return (current_user()['role'] ?? '') === 'staff'; + return auth_is('staff'); } // Back-compat shim: every is_designer() caller now means "is plain admin". @@ -125,21 +135,22 @@ function is_designer(): bool { return is_admin_role(); } +// Refreshes the session's user fields (name/email/role) without a full +// login — used after a profile edit or an admin-driven role change, where a +// full auth_login() (rehash checks, last_login stamp, audit "auth.login") +// would be the wrong event to log. Still rotates the session id: any +// privilege/credential change should get a fresh one. function auth_set_session(array $user): void { - // Rotate the session id on every privilege change (login, registration, - // password reset). Defeats session fixation: an attacker who pre-seeded - // FIT_SESS now ends up holding a dead id while the real user gets a fresh - // one. The CSRF token is rotated alongside the session id. + auth_session_regenerate(); if (session_status() === PHP_SESSION_ACTIVE) { - session_regenerate_id(true); unset($_SESSION['csrf_token']); } - $_SESSION['user_id'] = $user['id']; + $_SESSION['user_id'] = (int) $user['id']; $_SESSION['user'] = [ - 'id' => $user['id'], - 'name' => $user['name'], - 'email' => $user['email'], - 'role' => $user['role'], + 'id' => (int) $user['id'], + 'name' => $user['name'], + 'email' => $user['email'], + 'role' => $user['role'], ]; } diff --git a/config/db.php b/config/db.php index 7776411..28fed34 100644 --- a/config/db.php +++ b/config/db.php @@ -66,36 +66,57 @@ try { _bootstrap_fail('PDO connect failed: ' . $e->getMessage()); } -// Distinct session cookie per app so installs that share a host (e.g. -// development.andmore.pt/id.andmore.pt/ and /fitme3070.pt/) don't overwrite -// each other's PHPSESSID. Must run before session_start(). -// -// Cookie e modo estrito definidos aqui, no código, para valerem em qualquer -// servidor: antes o cookie saía só com path=/ (sem Secure, HttpOnly nem -// SameSite) e o PHP aceitava IDs de sessão inventados pelo cliente. -if (session_status() === PHP_SESSION_NONE) { - ini_set('session.use_strict_mode', '1'); - ini_set('session.use_only_cookies', '1'); - ini_set('session.cookie_httponly', '1'); - ini_set('session.cookie_samesite', 'Lax'); - if (request_is_https()) ini_set('session.cookie_secure', '1'); - session_name('FIT_SESS'); - session_start(); -} - // ─── Ligação ao platform-tools ─────────────────────────────────────────────── // -// A app mantém as suas próprias passwords e sessão (`config/auth.php`, -// `views/auth/login.php`) — não passa a usar o functions_auth.php partilhado -// (`'auth' => 'own'`), porque a `auth_attempts` desta app não tem a coluna -// `identifier` que o throttle partilhado exige (tem `ip`+`scope`, do seu -// próprio throttle em functions_throttle.php). Ainda assim liga-se ao -// bloqueio de IP da frota, ao router das páginas de sistema (Backups, -// Actualizações, Auditoria, IP bans, Geo, Origem das visitas, Hub) e ao -// resto do platform-tools, tudo por cima da autenticação própria. +// Auditoria 2026-09-24, ponto 1.3: a app passou a usar a autenticação +// partilhada (`shared/platform-tools/functions_auth.php`) em vez da sua +// própria — login, sessão, roles e guardas (config/auth.php, +// config/functions_user.php) agora chamam auth_attempt()/auth_login()/ +// auth_at_least()/etc. por baixo, com os mesmos nomes de função de sempre +// para os ~30 ficheiros que os chamam não mudarem. +// +// O cookie de sessão (nome, Secure/HttpOnly/SameSite, modo estrito) passa a +// ser inteiramente gerido por auth_session_start() através da opção +// 'session' abaixo — o bloco que estava aqui antes (ini_set + session_start +// manuais) foi removido para não haver dois sítios a configurar a mesma +// sessão. +// +// `users` já tem exactamente as colunas por omissão do auth_schema() +// (id, email, name, password_hash, role, is_active, last_login_at, +// last_login_ip — confirmado via information_schema em produção), por isso +// não é preciso mapear nada em 'schema'. +// +// auth_attempts (o throttle da APP, config/functions_throttle.php — ip+scope, +// cobre login/registo/recuperação) continua a ser o único throttle de login +// usado; o throttle do módulo partilhado (auth_throttle_check/hit, que exige +// `identifier`) não é chamado, para não duplicar a defesa do mesmo +// formulário com duas contagens diferentes. A tabela foi tornada conforme na +// mesma (database/migration_20260925_shared_auth_attempts.sql) para o +// relatório de saúde abaixo ('conformance' => 'report') não acusar erro e +// para deixar a porta aberta a usar o throttle partilhado no futuro. Ver +// views/auth/login.php para a nota completa. require_once dirname(__DIR__) . '/shared/platform-tools/functions_connector.php'; platform_connect($pdo, [ - 'auth' => 'own', + 'auth' => 'shared', + 'roles' => [ + // Preserva a hierarquia que config/auth.php já tinha (sysadmin > + // admin > staff > client, por in_array(), não por posto numérico): + // staff acima de client, e os dois abaixo de admin(80), para + // require_staff()/is_staff() continuarem a excluir clientes e + // require_admin()/is_admin() continuarem a excluir staff e clientes. + // sysadmin(100)/admin(80)/user(10) já vêm registados por omissão. + 'staff' => 40, + 'client' => 10, + ], + 'session' => [ + 'name' => 'FIT_SESS', + 'samesite' => 'Lax', + // secure/httponly ficam a cargo de auth_session_start(): secure + // segue o mesmo critério de HTTPS que o bloco antigo usava, + // httponly está sempre ligado. + ], + 'remember' => true, // «Manter sessão iniciada» — agora auth_remember_* + // do platform-tools; ver views/auth/login.php. 'guard' => 'require_sysadmin', 'router' => true, 'conformance' => 'report', // nunca recusa — só reporta no health endpoint diff --git a/config/functions.php b/config/functions.php index 699c3ca..1ad6cc0 100644 --- a/config/functions.php +++ b/config/functions.php @@ -15,9 +15,11 @@ require_once __DIR__ . '/functions_secrets.php'; // setting_or_env()/setting_s require_once __DIR__ . '/functions_db.php'; // generic DB helpers require_once __DIR__ . '/functions_audit.php'; // registo de auditoria (platform-tools) + audit() require_once __DIR__ . '/functions_trash.php'; // lixeira: soft delete + eliminação só após 30 dias -require_once __DIR__ . '/functions_user.php'; // auth_login/register, password reset, user creators +require_once __DIR__ . '/functions_user.php'; // register/reset/user creators (auth itself: platform-tools) require_once __DIR__ . '/auth.php'; // require_login/admin/staff/sysadmin(), role helpers -require_once __DIR__ . '/functions_remember.php'; // «Manter sessão iniciada» +// «Manter sessão iniciada»: shared/platform-tools/functions_remember.php, +// ligado por platform_connect(['remember' => true]) em config/db.php — já não +// há um functions_remember.php local (auditoria 1.3). require_once __DIR__ . '/functions_throttle.php'; // per-IP attempt throttle for the auth endpoints require_once __DIR__ . '/functions_migrations.php';// platform_update.php migrations runner diff --git a/config/functions_remember.php b/config/functions_remember.php deleted file mode 100644 index 5520633..0000000 --- a/config/functions_remember.php +++ /dev/null @@ -1,140 +0,0 @@ -:". O selector (32 hex) indexa a linha; só o -// SHA-256 do validator (64 hex) fica na BD, por isso um dump da tabela não dá -// para forjar um cookie. Cada utilização gasta o validator e emite outro — -// um cookie roubado só vale até a próxima visita do dono; quando o dono volta -// com um validator que já não bate certo, é a assinatura de uma cópia em -// circulação, e todos os tokens da conta são revogados. - -const REMEMBER_COOKIE = 'fit_remember'; -const REMEMBER_DAYS = 30; - -function remember_set_cookie(?string $value, int $expires = 0): void { - if (headers_sent()) return; - setcookie(REMEMBER_COOKIE, (string) $value, [ - 'expires' => $expires, - 'path' => '/', - 'secure' => request_is_https(), - 'httponly' => true, - // Lax, não Strict: um link de fora (email) não traz um cookie Strict, - // e a pessoa via o login outra vez apesar de ter pedido para ficar. - 'samesite' => 'Lax', - ]); -} - -function remember_forget_cookie(): void { - remember_set_cookie('', time() - 3600); -} - -function remember_parse_cookie(): ?array { - $raw = (string) ($_COOKIE[REMEMBER_COOKIE] ?? ''); - if ($raw === '' || substr_count($raw, ':') !== 1) return null; - [$selector, $validator] = explode(':', $raw, 2); - if (strlen($selector) !== 32 || !ctype_xdigit($selector)) return null; - if (strlen($validator) !== 64 || !ctype_xdigit($validator)) return null; - return ['selector' => $selector, 'validator' => $validator]; -} - -// Chamar depois de um login bem sucedido, só quando a pessoa pediu para ficar. -function remember_issue(PDO $pdo, int $userId): void { - if ($userId <= 0) return; - $selector = bin2hex(random_bytes(16)); - $validator = bin2hex(random_bytes(32)); - $expires = time() + REMEMBER_DAYS * 86400; - try { - $pdo->prepare( - "INSERT INTO remember_tokens (user_id, selector, validator_hash, expires_at, last_ip, user_agent) - VALUES (?, ?, ?, ?, ?, ?)" - )->execute([ - $userId, $selector, hash('sha256', $validator), date('Y-m-d H:i:s', $expires), - client_ip(), mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), - ]); - } catch (Throwable $e) { - // Um login válido nunca deve falhar por causa disto. - error_log('[remember] issue failed: ' . $e->getMessage()); - return; - } - remember_set_cookie($selector . ':' . $validator, $expires); - if (random_int(0, 19) === 0) remember_prune($pdo); -} - -// Repõe a sessão a partir do cookie, se houver um válido. Não redirecciona. -function remember_resume(PDO $pdo): void { - if (!empty($_SESSION['user_id'])) return; - $cookie = remember_parse_cookie(); - if ($cookie === null) return; - - try { - $st = $pdo->prepare("SELECT * FROM remember_tokens WHERE selector = ? LIMIT 1"); - $st->execute([$cookie['selector']]); - $row = $st->fetch(PDO::FETCH_ASSOC) ?: null; - } catch (Throwable $e) { - error_log('[remember] lookup failed: ' . $e->getMessage()); - return; - } - - if ($row === null) { - remember_forget_cookie(); - return; - } - - if (!hash_equals((string) $row['validator_hash'], hash('sha256', $cookie['validator']))) { - error_log('[remember] validator mismatch for user ' . (int) $row['user_id'] . ' — a revogar todos os tokens da conta'); - remember_forget_user($pdo, (int) $row['user_id']); - remember_forget_cookie(); - return; - } - - if (strtotime((string) $row['expires_at']) <= time()) { - remember_delete($pdo, (string) $row['selector']); - remember_forget_cookie(); - return; - } - - $st = $pdo->prepare("SELECT * FROM users WHERE id = ? AND is_active = 1 LIMIT 1"); - $st->execute([(int) $row['user_id']]); - $user = $st->fetch(PDO::FETCH_ASSOC) ?: null; - if ($user === null) { - remember_forget_user($pdo, (int) $row['user_id']); - remember_forget_cookie(); - return; - } - - auth_set_session($user); - $_SESSION['_remembered'] = true; - remember_rotate($pdo, (string) $row['selector']); -} - -function remember_rotate(PDO $pdo, string $selector): void { - $validator = bin2hex(random_bytes(32)); - $expires = time() + REMEMBER_DAYS * 86400; - try { - $pdo->prepare("UPDATE remember_tokens SET validator_hash = ?, expires_at = ?, last_ip = ? WHERE selector = ?") - ->execute([hash('sha256', $validator), date('Y-m-d H:i:s', $expires), client_ip(), $selector]); - } catch (Throwable $e) { - error_log('[remember] rotate failed: ' . $e->getMessage()); - return; - } - remember_set_cookie($selector . ':' . $validator, $expires); -} - -function remember_delete(PDO $pdo, string $selector): void { - try { $pdo->prepare("DELETE FROM remember_tokens WHERE selector = ?")->execute([$selector]); } - catch (Throwable $e) { error_log('[remember] delete failed: ' . $e->getMessage()); } -} - -// Chamar no logout e ao mudar a palavra-passe — corta qualquer cookie roubado. -function remember_forget_user(PDO $pdo, int $userId): void { - try { $pdo->prepare("DELETE FROM remember_tokens WHERE user_id = ?")->execute([$userId]); } - catch (Throwable $e) { error_log('[remember] forget_user failed: ' . $e->getMessage()); } -} - -function remember_prune(PDO $pdo): void { - try { $pdo->exec("DELETE FROM remember_tokens WHERE expires_at < NOW()"); } - catch (Throwable $e) { /* accessório */ } -} diff --git a/config/functions_user.php b/config/functions_user.php index dec0840..c18cba2 100644 --- a/config/functions_user.php +++ b/config/functions_user.php @@ -1,8 +1,11 @@ prepare( - "SELECT * FROM users WHERE email = ? AND is_active = 1 AND password_hash IS NOT NULL LIMIT 1" - ); - $stmt->execute([strtolower(trim($email))]); - $user = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!$user) { - // Sem conta, verifica na mesma contra um hash de referência: um bcrypt - // de custo 12 demora dezenas de ms, e responder mais depressa quando o - // email não existe diria a quem tenta quais emails têm conta. - password_verify($password, AUTH_DUMMY_HASH); - return null; - } - if (!password_verify($password, $user['password_hash'])) { - return null; - } - - // Transparently upgrade hashes when PHP's default algorithm/cost moves on. - if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) { - $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?") - ->execute([password_hash($password, PASSWORD_DEFAULT), $user['id']]); - } - - $ip = client_ip(); +// Stamps the user-agent of the browser that just logged in. Best-effort: +// shown in views/user/index.php ("last seen from …") but never worth failing +// a login over. +function fit_stamp_login_ua(PDO $pdo, int $userId): void { $ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512) ?: null; - $pdo->prepare("UPDATE users SET last_login_at = NOW(), last_login_ip = ?, last_login_ua = ? WHERE id = ?") - ->execute([$ip, $ua, $user['id']]); - $user['last_login_at'] = date('Y-m-d H:i:s'); - $user['last_login_ip'] = $ip; - $user['last_login_ua'] = $ua; - return $user; + try { + $pdo->prepare("UPDATE users SET last_login_ua = ? WHERE id = ?") + ->execute([$ua, $userId]); + } catch (Throwable $e) { + error_log('[auth] last_login_ua stamp failed: ' . $e->getMessage()); + } } // Writes a new password hash. Shared by the reset flow, the profile page and @@ -74,10 +59,10 @@ function auth_login(PDO $pdo, string $email, string $password): ?array { // take the account back. function auth_set_password(PDO $pdo, int $userId, string $password): void { $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?") - ->execute([password_hash($password, PASSWORD_DEFAULT), $userId]); + ->execute([auth_password_hash($password), $userId]); // Uma password nova invalida qualquer «manter sessão iniciada» antigo — // sem isto, um cookie roubado antes da mudança continuava a entrar depois. - remember_forget_user($pdo, $userId); + auth_remember_forget_user($pdo, $userId); $self = (int) ($_SESSION['user_id'] ?? 0) === $userId; audit($self ? 'auth.password_change' : 'user.password_set', [ 'target_type' => 'user', 'target_id' => $userId, @@ -123,7 +108,7 @@ function auth_register_client(PDO $pdo, array $data): array { } $pdo->prepare( "UPDATE users SET name = ?, phone = COALESCE(?, phone), password_hash = ?, is_active = 1 WHERE id = ?" - )->execute([$name, $phone, password_hash($data['password'], PASSWORD_DEFAULT), $row['id']]); + )->execute([$name, $phone, auth_password_hash($data['password']), $row['id']]); audit('user.register', ['target_type' => 'user', 'target_id' => (int) $row['id'], 'target_label' => $name, 'summary' => 'Registo que reclamou uma conta de cliente sem palavra-passe', 'actor' => null]); return ['id' => (int) $row['id']]; @@ -139,7 +124,7 @@ function auth_register_client(PDO $pdo, array $data): array { $name, $email, $phone, - password_hash($data['password'], PASSWORD_DEFAULT), + auth_password_hash($data['password']), !empty($data['marketing_consent']) ? 1 : 0, ]); } catch (PDOException $e) { @@ -164,7 +149,7 @@ function auth_create_sysadmin(PDO $pdo, string $name, string $email, string $pas $stmt->execute([ $name, strtolower(trim($email)), - password_hash($password, PASSWORD_DEFAULT), + auth_password_hash($password), ]); $id = (int) $pdo->lastInsertId(); audit('user.create', ['target_type' => 'user', 'target_id' => $id, 'target_label' => $name, @@ -193,7 +178,7 @@ function _create_backoffice_user(PDO $pdo, string $role, string $name, string $e $name, strtolower(trim($email)), $phone ?: null, - password_hash($password, PASSWORD_DEFAULT), + auth_password_hash($password), $role, ]); } catch (PDOException $e) { diff --git a/database/migration_20260925_shared_auth_attempts.sql b/database/migration_20260925_shared_auth_attempts.sql new file mode 100644 index 0000000..638645e --- /dev/null +++ b/database/migration_20260925_shared_auth_attempts.sql @@ -0,0 +1,34 @@ +-- ─── auth_attempts: torna-a conforme com o throttle do platform-tools ──────── +-- Auditoria 2026-09-24, ponto 1.3: a app passa a usar o auth partilhado +-- (shared/platform-tools/functions_auth.php) em vez do seu próprio login/ +-- sessão/roles. Ver config/db.php ('auth' => 'shared'). +-- +-- Esta tabela já existia (migration_20260913_local_auth.sql) com o formato do +-- throttle PRÓPRIO da app (config/functions_throttle.php: `ip` + `scope`, +-- cobre login/registo/recuperação). O throttle partilhado +-- (auth_throttle_check/hit em functions_auth.php) espera `identifier` + `ip` +-- e falharia (INSERT INTO auth_attempts (identifier, ip) ...) com um erro de +-- SQL — silenciosamente engolido pelo try/catch do throttle partilhado, que +-- por desenho falha aberto e deixaria de bloquear ninguém. +-- +-- Decisão: a app CONTINUA a usar só o seu próprio throttle (login+registo+ +-- recuperação, dois níveis IP+conta) — não passa a chamar +-- auth_throttle_check()/auth_throttle_hit() em lado nenhum, para não duplicar +-- a defesa contra o mesmo formulário com duas contagens diferentes. Esta +-- migração só torna a TABELA conforme com o que o platform_conformance() +-- (config/db.php, 'conformance' => 'report') verifica, para o relatório de +-- saúde ficar limpo e para deixar a porta aberta a usar o throttle partilhado +-- no futuro sem precisar de outra migração. +-- +-- `identifier` e `success` são aditivos, com omissão — invisíveis para o +-- INSERT/SELECT do throttle próprio, que continua a nomear só `ip`+`scope`. +-- `scope` passa a ter omissão (era NOT NULL sem omissão) porque é a única +-- coluna que o INSERT do throttle partilhado não preenche; sem omissão esse +-- INSERT falhava em modo estrito. +-- +-- Safe to re-run (IF NOT EXISTS é extensão do MariaDB). + +ALTER TABLE `auth_attempts` ADD COLUMN IF NOT EXISTS `identifier` VARCHAR(190) NOT NULL DEFAULT '' AFTER `id`; +ALTER TABLE `auth_attempts` ADD COLUMN IF NOT EXISTS `success` TINYINT(1) NOT NULL DEFAULT 0 AFTER `attempted_at`; +ALTER TABLE `auth_attempts` MODIFY COLUMN `scope` VARCHAR(20) NOT NULL DEFAULT ''; +ALTER TABLE `auth_attempts` ADD INDEX IF NOT EXISTS `idx_identifier_time` (`identifier`, `attempted_at`); diff --git a/views/auth/login.php b/views/auth/login.php index 75ece4d..84469b7 100644 --- a/views/auth/login.php +++ b/views/auth/login.php @@ -1,6 +1,16 @@ 'user', 'target_id' => (int) $user['id'], 'target_label' => (string) $user['name']]); header('Location: ' . base_url($destination($user['role']))); exit; } diff --git a/views/auth/logout.php b/views/auth/logout.php index 71574ab..215d942 100644 --- a/views/auth/logout.php +++ b/views/auth/logout.php @@ -1,25 +1,18 @@ 'user', 'target_id' => (int) $_SESSION['user_id']]); +if (auth_check()) { // Só o token deste browser, não toda a conta: sair aqui não deve expulsar // a pessoa de outro dispositivo onde pediu para ficar. - $cookie = remember_parse_cookie(); - if ($cookie !== null) remember_delete($pdo, $cookie['selector']); + auth_remember_clear($pdo); } -remember_forget_cookie(); -$_SESSION = []; -// Drop the cookie too, so the browser doesn't keep presenting a dead id. -if (ini_get('session.use_cookies')) { - $p = session_get_cookie_params(); - setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']); -} -session_destroy(); +auth_logout(); header('Location: ' . base_url('views/auth/login.php?loggedout=1')); exit; diff --git a/views/auth/register.php b/views/auth/register.php index f84b5c8..816884b 100644 --- a/views/auth/register.php +++ b/views/auth/register.php @@ -51,7 +51,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // login form with credentials they just typed. $user = db_get_user($pdo, (int) $res['id']); if ($user) { - auth_set_session($user); + // auth_login() (shared) — same primitive views/auth/login.php + // uses, so a fresh registration and a normal login leave the + // session/audit trail in the same shape. + auth_login($pdo, $user); header('Location: ' . base_url(post_login_destination($user['role']))); exit; }