Restructure
New Journal entries
Colour tabs update with wind
This commit is contained in:
fraxle
2026-08-31 15:12:45 +01:00
parent 47818fba33
commit 71860b9dd9
40 changed files with 3430 additions and 443 deletions
+68
View File
@@ -0,0 +1,68 @@
<?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;
}
+8
View File
@@ -0,0 +1,8 @@
<?php
// ─── SunScope — API bootstrap ─────────────────────────────────────────────
// require'd at the top of every api/*.php endpoint.
require __DIR__ . '/../secrets.local.php';
require __DIR__ . '/db.php';
require __DIR__ . '/http.php';
require __DIR__ . '/auth.php';
+21
View File
@@ -0,0 +1,21 @@
<?php
// ─── SunScope — Database connection ──────────────────────────────────────
// One shared PDO connection, MySQL/MariaDB via XAMPP locally. Credentials
// live in secrets.local.php alongside the Stripe key, never committed.
function db(): PDO {
static $pdo = null;
if ($pdo !== null) return $pdo;
$pdo = new PDO(
'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
return $pdo;
}
+24
View File
@@ -0,0 +1,24 @@
<?php
// ─── SunScope — JSON response helpers ────────────────────────────────────
function json_out(array $data, int $status = 200): void {
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
function json_error(string $message, int $status = 400): void {
json_out(['ok' => false, 'error' => $message], $status);
}
function json_body(): array {
$body = json_decode(file_get_contents('php://input'), true);
return is_array($body) ? $body : [];
}
function require_method(string $method): void {
if ($_SERVER['REQUEST_METHOD'] !== $method) {
json_error('Method not allowed', 405);
}
}