Files
sunscope/lib/auth.php
T
fraxle 71860b9dd9 5.4.0
Restructure
New Journal entries
Colour tabs update with wind
2026-08-31 15:12:45 +01:00

69 lines
2.4 KiB
PHP

<?php
// ─── SunScope — Session auth ──────────────────────────────────────────────
// Opaque DB-backed session token in an HttpOnly cookie, not $_SESSION —
// keeps the door open for a future magic-link flow without a rewrite, and
// means a session can be revoked by deleting one row.
//
// The cookie holds the plaintext token; only its SHA-256 hash is stored,
// same pattern as a password hash, so a DB read alone can't forge a login.
const SESSION_COOKIE = 'ss_session';
const SESSION_DAYS = 30;
function new_public_id(): string {
return rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '=');
}
function issue_session(int $userId): void {
$token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$hash = hash('sha256', $token, true);
$expiresAt = (new DateTime())->modify('+' . SESSION_DAYS . ' days')->format('Y-m-d H:i:s');
$stmt = db()->prepare(
'INSERT INTO sessions (user_id, token_hash, expires_at) VALUES (?, ?, ?)'
);
$stmt->execute([$userId, $hash, $expiresAt]);
setcookie(SESSION_COOKIE, $token, [
'expires' => time() + SESSION_DAYS * 86400,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
// 'secure' left off for local XAMPP http:// dev; set true behind HTTPS in production.
]);
}
function clear_session(): void {
$token = $_COOKIE[SESSION_COOKIE] ?? null;
if ($token) {
$hash = hash('sha256', $token, true);
$stmt = db()->prepare('DELETE FROM sessions WHERE token_hash = ?');
$stmt->execute([$hash]);
}
setcookie(SESSION_COOKIE, '', ['expires' => time() - 3600, 'path' => '/']);
}
// Returns the logged-in user's row, or null if there is no valid session.
function current_user(): ?array {
$token = $_COOKIE[SESSION_COOKIE] ?? null;
if (!$token) return null;
$hash = hash('sha256', $token, true);
$stmt = db()->prepare(
'SELECT u.id, u.public_id, u.email
FROM sessions s
JOIN users u ON u.id = s.user_id
WHERE s.token_hash = ? AND s.expires_at > NOW()'
);
$stmt->execute([$hash]);
$row = $stmt->fetch();
return $row ?: null;
}
// Ends the request with 401 if not logged in; otherwise returns the user row.
function require_user(): array {
$user = current_user();
if (!$user) json_error('Not signed in', 401);
return $user;
}