0, 'path' => '/', 'secure' => $https, 'httponly' => true, 'samesite' => 'Lax', ]); ini_set('session.use_strict_mode', '1'); session_start(); } /* ---------- Current user ---------- */ function current_user(): ?array { if (empty($_SESSION['uid'])) return null; static $u = null; if ($u !== null) return $u ?: null; $st = db()->prepare('SELECT * FROM users WHERE id = ? AND status = "active" LIMIT 1'); $st->execute([$_SESSION['uid']]); $u = $st->fetch() ?: false; if (!$u) { logout_user(); return null; } return $u; } function is_logged_in(): bool { return current_user() !== null; } function is_admin(): bool { $u = current_user(); return $u && $u['role'] === 'admin'; } function require_login(): void { if (!is_logged_in()) { flash('Please sign in to continue.'); redirect('/login.php'); } } function require_admin(): void { require_login(); if (!is_admin()) { http_response_code(403); exit('Forbidden.'); } } /* ---------- Login / logout ---------- */ function login_user(array $user): void { session_regenerate_id(true); // prevent session fixation $_SESSION['uid'] = (int)$user['id']; db()->prepare('UPDATE users SET last_login_at = NOW() WHERE id = ?') ->execute([$user['id']]); audit('login', $user['email'], (int)$user['id']); } function logout_user(): void { $_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(); } /* ---------- Google OAuth 2.0 (dependency-free) ---------- */ function google_auth_url(): string { $_SESSION['oauth_state'] = bin2hex(random_bytes(16)); $params = [ 'client_id' => GOOGLE_CLIENT_ID, 'redirect_uri' => BASE_URL . '/oauth-callback.php', 'response_type' => 'code', 'scope' => 'openid email profile', 'state' => $_SESSION['oauth_state'], 'access_type' => 'online', 'prompt' => 'select_account', ]; return 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query($params); } /** Exchange code for tokens, then fetch the verified profile. */ function google_fetch_profile(string $code): ?array { // 1) code -> tokens $tok = http_post('https://oauth2.googleapis.com/token', [ 'code' => $code, 'client_id' => GOOGLE_CLIENT_ID, 'client_secret' => GOOGLE_CLIENT_SECRET, 'redirect_uri' => BASE_URL . '/oauth-callback.php', 'grant_type' => 'authorization_code', ]); if (!$tok || empty($tok['access_token'])) return null; // 2) tokens -> userinfo $info = http_get('https://openidconnect.googleapis.com/v1/userinfo', $tok['access_token']); if (!$info || empty($info['sub']) || empty($info['email'])) return null; if (isset($info['email_verified']) && $info['email_verified'] === false) return null; return $info; } /** Create or update the user, applying first-admin bootstrap. */ function upsert_google_user(array $p): array { $pdo = db(); $st = $pdo->prepare('SELECT * FROM users WHERE google_id = ? OR email = ? LIMIT 1'); $st->execute([$p['sub'], $p['email']]); $existing = $st->fetch(); $name = $p['name'] ?? ''; $pic = $p['picture'] ?? ''; if ($existing) { $pdo->prepare('UPDATE users SET google_id=?, name=?, picture=? WHERE id=?') ->execute([$p['sub'], $name, $pic, $existing['id']]); $st->execute([$p['sub'], $p['email']]); return $st->fetch(); } // New user. Bootstrap admin if this is the configured first-admin email. $role = (strtolower($p['email']) === strtolower(BOOTSTRAP_ADMIN_EMAIL)) ? 'admin' : 'user'; $ins = $pdo->prepare( 'INSERT INTO users (google_id, email, name, picture, role) VALUES (?,?,?,?,?)'); $ins->execute([$p['sub'], $p['email'], $name, $pic, $role]); $id = (int)$pdo->lastInsertId(); audit('signup', $p['email'] . ($role === 'admin' ? ' (bootstrap admin)' : ''), $id); $st->execute([$p['sub'], $p['email']]); return $st->fetch(); } /* ---------- Minimal HTTP helpers (cURL) ---------- */ function http_post(string $url, array $data): ?array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($data), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); $res = curl_exit_json($ch); return $res; } function http_get(string $url, string $bearer): ?array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $bearer], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); return curl_exit_json($ch); } function curl_exit_json($ch): ?array { $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; $j = json_decode($body, true); return is_array($j) ? $j : null; }