170 lines
6.3 KiB
PHP
170 lines
6.3 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* Database connection & schema management for SQLite
|
||
*/
|
||
|
||
function get_db(): PDO {
|
||
static $pdo = null;
|
||
if ($pdo !== null) {
|
||
return $pdo;
|
||
}
|
||
|
||
// In Docker, FITZ_DB_PATH points outside the web root (see
|
||
// docker-compose.yaml), so Apache can never serve the file. Local dev
|
||
// falls back to db/ in the project, which the dev router refuses to serve.
|
||
$dbPath = getenv('FITZ_DB_PATH') ?: __DIR__ . '/../db/fitz.sqlite';
|
||
$dbDir = dirname($dbPath);
|
||
if (!is_dir($dbDir)) {
|
||
mkdir($dbDir, 0755, true);
|
||
}
|
||
|
||
$isNew = !file_exists($dbPath);
|
||
|
||
$pdo = new PDO('sqlite:' . $dbPath, null, null, [
|
||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||
PDO::ATTR_TIMEOUT => 5
|
||
]);
|
||
|
||
// Enable WAL mode for concurrency and performance
|
||
$pdo->exec('PRAGMA journal_mode = WAL;');
|
||
$pdo->exec('PRAGMA foreign_keys = ON;');
|
||
|
||
// Initialize tables if needed
|
||
init_db_schema($pdo);
|
||
|
||
return $pdo;
|
||
}
|
||
|
||
function init_db_schema(PDO $pdo): void {
|
||
$pdo->exec("
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
username TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
role TEXT NOT NULL DEFAULT 'admin',
|
||
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS announcements (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
type TEXT NOT NULL DEFAULT 'holiday',
|
||
headline TEXT NOT NULL,
|
||
message TEXT NOT NULL,
|
||
start_date TEXT NULL,
|
||
end_date TEXT NULL,
|
||
show_banner INTEGER NOT NULL DEFAULT 1,
|
||
show_in_hours INTEGER NOT NULL DEFAULT 1,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS menu_categories (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
description TEXT NULL,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS menu_items (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
category_id INTEGER NOT NULL REFERENCES menu_categories(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
description TEXT NULL,
|
||
price TEXT NOT NULL,
|
||
badge TEXT NULL,
|
||
is_available INTEGER NOT NULL DEFAULT 1,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS pub_hours (
|
||
day_of_week INTEGER PRIMARY KEY,
|
||
is_open INTEGER NOT NULL DEFAULT 0,
|
||
open_time TEXT NULL,
|
||
close_time TEXT NULL,
|
||
kitchen_close_time TEXT NULL,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- Single-row table (id is pinned to 1) for site-wide toggles that
|
||
-- don't belong to any one record — starting with whether the
|
||
-- public menu page shows its items at all.
|
||
CREATE TABLE IF NOT EXISTS site_settings (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
menu_enabled INTEGER NOT NULL DEFAULT 1,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- Failed admin logins, used by inc/login-throttle.php. Rows older
|
||
-- than the throttle window are pruned on every new failure.
|
||
CREATE TABLE IF NOT EXISTS login_attempts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
ip TEXT NOT NULL,
|
||
username TEXT NOT NULL,
|
||
attempted_at INTEGER NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_login_attempts_ip ON login_attempts (ip, attempted_at);
|
||
CREATE INDEX IF NOT EXISTS idx_login_attempts_username ON login_attempts (username, attempted_at);
|
||
");
|
||
|
||
// users.is_enabled shipped after the initial release — CREATE TABLE IF
|
||
// NOT EXISTS above is a no-op against an existing table, so an install
|
||
// from before this needs the column added explicitly, once.
|
||
$hasIsEnabled = false;
|
||
foreach ($pdo->query("PRAGMA table_info(users)") as $col) {
|
||
if ($col['name'] === 'is_enabled') {
|
||
$hasIsEnabled = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$hasIsEnabled) {
|
||
$pdo->exec("ALTER TABLE users ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1");
|
||
}
|
||
|
||
// No default admin is seeded — a fresh database has no users until one
|
||
// is created from the CLI (run as www-data so the sqlite file stays
|
||
// writable by Apache):
|
||
// docker exec -it -u www-data the-old-fitz php /var/www/html/bin/create-user.php
|
||
|
||
// Seed default weekly hours if the table is empty: closed Sun–Wed,
|
||
// 4–10pm Thu–Sat with the kitchen closing at 9 — matches the hours the
|
||
// site originally shipped with as hardcoded copy.
|
||
$stmt = $pdo->query("SELECT COUNT(*) as count FROM pub_hours");
|
||
$hoursCount = $stmt->fetchColumn();
|
||
if ($hoursCount == 0) {
|
||
$insert = $pdo->prepare("
|
||
INSERT INTO pub_hours (day_of_week, is_open, open_time, close_time, kitchen_close_time)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
");
|
||
// 0=Sunday ... 6=Saturday
|
||
$defaults = [
|
||
0 => null, // Sunday — closed
|
||
1 => null, // Monday — closed
|
||
2 => null, // Tuesday — closed
|
||
3 => null, // Wednesday — closed
|
||
4 => ['16:00', '22:00', '21:00'], // Thursday
|
||
5 => ['16:00', '22:00', '21:00'], // Friday
|
||
6 => ['16:00', '22:00', '21:00'], // Saturday
|
||
];
|
||
foreach ($defaults as $day => $times) {
|
||
if ($times === null) {
|
||
$insert->execute([$day, 0, null, null, null]);
|
||
} else {
|
||
$insert->execute([$day, 1, $times[0], $times[1], $times[2]]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Seed the singleton settings row (menu on by default) if it doesn't exist yet.
|
||
$stmt = $pdo->query("SELECT COUNT(*) as count FROM site_settings");
|
||
$settingsCount = $stmt->fetchColumn();
|
||
if ($settingsCount == 0) {
|
||
$pdo->exec("INSERT INTO site_settings (id, menu_enabled) VALUES (1, 1)");
|
||
}
|
||
}
|