Config::int('SESSION_LIFETIME', 28800), 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } public static function isAuthenticated(): bool { self::startSession(); return !empty($_SESSION['authenticated']) && !empty($_SESSION['auth_time']) && (time() - (int)$_SESSION['auth_time']) < Config::int('SESSION_LIFETIME', 28800); } public static function user(): array { self::startSession(); return [ 'email' => $_SESSION['user_email'] ?? '', 'name' => $_SESSION['user_name'] ?? '', ]; } public static function buildLoginUrl(?string $returnTo = null): string { self::startSession(); $state = bin2hex(random_bytes(16)); $_SESSION['oauth_state'] = $state; $_SESSION['oauth_return_to'] = $returnTo ?: ($_SERVER['REQUEST_URI'] ?? '/'); $params = http_build_query([ 'response_type' => 'code', 'client_id' => Config::get('OAUTH_CLIENT_ID'), 'redirect_uri' => Config::get('OAUTH_REDIRECT_URI'), 'scope' => Config::get('OAUTH_SCOPE', 'profile'), 'state' => $state, ]); return Config::get('OAUTH_AUTH_URL') . '?' . $params; } public static function requireAuth(): void { if (self::isAuthenticated()) return; $loginUrl = self::buildLoginUrl(); $accepts = $_SERVER['HTTP_ACCEPT'] ?? ''; $isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || str_contains($accepts, 'application/json'); if ($isAjax) { header('Content-Type: application/json; charset=utf-8'); http_response_code(401); echo json_encode(['ok' => false, 'auth_required' => true, 'login_url' => $loginUrl]); exit; } header('Location: ' . $loginUrl); exit; } public static function emailDomainAllowed(string $email): bool { $allowed = Config::list('ALLOWED_EMAIL_DOMAINS'); if (!$allowed) return true; $parts = explode('@', strtolower(trim($email))); if (count($parts) !== 2) return false; return in_array($parts[1], array_map('strtolower', $allowed), true); } public static function completeLogin(string $email, string $name): void { self::startSession(); session_regenerate_id(true); $_SESSION['authenticated'] = true; $_SESSION['user_email'] = $email; $_SESSION['user_name'] = $name ?: $email; $_SESSION['auth_time'] = time(); try { $pdo = Db::pdo(); $stmt = $pdo->prepare( 'INSERT INTO users (email, name, first_seen_at, last_login_at) VALUES (:email, :name, NOW(), NOW()) ON DUPLICATE KEY UPDATE name = VALUES(name), last_login_at = NOW()' ); $stmt->execute([':email' => $email, ':name' => $name]); } catch (Throwable $e) { error_log('users upsert failed: ' . $e->getMessage()); } } public static function logout(): void { self::startSession(); $_SESSION = []; 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(); } public static function popReturnTo(): string { self::startSession(); $to = $_SESSION['oauth_return_to'] ?? '/'; unset($_SESSION['oauth_return_to']); return $to; } public static function checkState(string $state): bool { self::startSession(); $expected = $_SESSION['oauth_state'] ?? ''; unset($_SESSION['oauth_state']); return $state !== '' && hash_equals((string)$expected, $state); } }