'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, '/'); }