69 lines
2.4 KiB
PHP
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;
|
|
}
|