config/db.php liga-se com 'auth' => 'shared' em vez de 'own': login, sessão, papéis e guardas passam a correr sobre shared/platform-tools/functions_auth.php (auth_attempt/auth_login/ auth_at_least/auth_is/auth_session_start/auth_remember_*) em vez da lógica local duplicada. Os ~30 ficheiros em views/ que chamam require_login()/require_admin()/require_staff()/require_sysadmin()/ is_admin()/etc. não mudam — só o que está por baixo dessas funções. - roles: staff=40, client=10 registados para preservar a hierarquia sysadmin>admin>staff>client que os antigos in_array() garantiam. `users` já tinha exactamente as colunas por omissão do auth_schema(), não foi preciso mapear nada. - sessão: o bloco manual de ini_set()/session_start() em config/db.php foi substituído pela opção 'session' do platform_connect() — mesmo nome de cookie (FIT_SESS), mesmo SameSite=Lax, secure/httponly passam a ser geridos por auth_session_start(). - «Manter sessão iniciada»: config/functions_remember.php (a reimplementação local do selector/validator) foi eliminado a favor do módulo partilhado (auth_remember_issue/resume/clear via 'remember' => true), que já cobria o mesmo desenho. - auth_attempts: tinha (ip, scope) do throttle próprio da app (config/functions_throttle.php), não o `identifier` que o throttle partilhado exige. Migração aditiva (migration_20260925_shared_auth_attempts.sql) torna a tabela conforme sem tocar no throttle da app, que continua a ser o único usado para login/registo/recuperação — o throttle partilhado não é chamado, para não duplicar a mesma defesa com duas contagens. - last_login_ua (sem equivalente partilhado) passa a ser gravado por fit_stamp_login_ua(), chamado a seguir a auth_login(). Testado ponta-a-ponta num contentor scratch na VPS (site-fitme:8.4, cópia da BD de produção + migração aplicada): visitante anónimo é redirigido, password errada dá o mesmo erro genérico e conta para o throttle (bloqueia à 5ª tentativa, 429), login correcto regenera a sessão, grava last_login_at/ip/ua, audita "auth.login" e (com "lembrar-me") emite o cookie am_remember; esse cookie sozinho, numa sessão nova, repõe a sessão certa com rotação do validator; logout limpa sessão+cookie+token e audita "auth.logout"; platform.php (guard=require_sysadmin) e views/admin/dashboard.php respondem 302 sem sessão e 200 com sessão de sysadmin. php -l limpo em todos os ficheiros alterados. Migração já aplicada em produção (aditiva). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
173 lines
6.8 KiB
PHP
173 lines
6.8 KiB
PHP
<?php
|
|
// Auth guards — require functions.php (and the shared session, started by
|
|
// platform_connect() in config/db.php) before this file.
|
|
//
|
|
// Auditoria 2026-09-24, ponto 1.3: estas funções deixaram de ter lógica
|
|
// própria de sessão/roles — delegam em
|
|
// shared/platform-tools/functions_auth.php (auth_check/auth_user/
|
|
// auth_at_least/auth_is/...), ligado em config/db.php ('auth' => '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).
|
|
// admin — full staff access except those three sysadmin-only items.
|
|
// staff — works only on items assigned to them (e.g. sessions where
|
|
// photographer_id = users.id). Cannot manage other users,
|
|
// templates, settings, etc.
|
|
// 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.
|
|
function post_login_destination(string $role): string {
|
|
return match ($role) {
|
|
'sysadmin', 'admin' => 'views/admin/dashboard.php',
|
|
'staff' => 'views/staff/dashboard.php',
|
|
'client' => 'views/user/dashboard.php',
|
|
default => 'index.php',
|
|
};
|
|
}
|
|
|
|
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 (!auth_at_least('admin')) {
|
|
header('Location: ' . base_url(post_login_destination($user['role'] ?? '')));
|
|
exit;
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
// Accepts sysadmin + admin + staff — anyone who can act on assignments.
|
|
// Clients are redirected to the public index.
|
|
function require_staff(): array {
|
|
$user = require_login();
|
|
if (!auth_at_least('staff')) {
|
|
header('Location: ' . base_url('index.php'));
|
|
exit;
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
// Ownership guard for staff-scoped pages (edit/update_status/send_email/etc).
|
|
// Sysadmin and admin pass for any session. Plain staff only pass when the
|
|
// session's photographer_id matches their user id. Mismatches bounce back to
|
|
// the role-appropriate sessions list.
|
|
function require_session_ownership(array $session): array {
|
|
$user = require_staff();
|
|
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;
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
// Sysadmin-only pages (backups, platform update, sensitive settings).
|
|
// Plain admins are sent back to their dashboard.
|
|
function require_sysadmin(): array {
|
|
$user = require_login();
|
|
if (!auth_is('sysadmin')) {
|
|
header('Location: ' . base_url(post_login_destination($user['role'] ?? '')));
|
|
exit;
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
function current_user(): ?array {
|
|
return auth_user();
|
|
}
|
|
|
|
// True for sysadmin + admin (NOT plain staff).
|
|
function is_admin(): bool {
|
|
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 auth_at_least('staff');
|
|
}
|
|
|
|
function is_sysadmin(): bool {
|
|
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 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 auth_is('staff');
|
|
}
|
|
|
|
// Back-compat shim: every is_designer() caller now means "is plain admin".
|
|
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 {
|
|
auth_session_regenerate();
|
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
unset($_SESSION['csrf_token']);
|
|
}
|
|
$_SESSION['user_id'] = (int) $user['id'];
|
|
$_SESSION['user'] = [
|
|
'id' => (int) $user['id'],
|
|
'name' => $user['name'],
|
|
'email' => $user['email'],
|
|
'role' => $user['role'],
|
|
];
|
|
}
|
|
|
|
// Sanitises a ?return_to= value into a path safe to redirect to after login.
|
|
// Anything absolute, protocol-relative or backslash-prefixed is discarded so a
|
|
// crafted login link can't bounce the user off-site with a fresh session in hand.
|
|
function safe_return_path(?string $raw): string {
|
|
$raw = trim((string) $raw);
|
|
if ($raw === '' || $raw[0] !== '/') return '';
|
|
if (str_starts_with($raw, '//') || str_starts_with($raw, '/' . chr(92))) return '';
|
|
|
|
// REQUEST_URI includes the deployment base (e.g. /fitme3070.pt/…) while
|
|
// base_url() adds it back, so strip it to avoid doubling the prefix.
|
|
$base = parse_url(APP_BASE_URL, PHP_URL_PATH) ?: '/';
|
|
if ($base !== '/' && str_starts_with($raw, $base)) {
|
|
$raw = substr($raw, strlen($base));
|
|
}
|
|
return ltrim($raw, '/');
|
|
}
|