Files
theoldfitz/inc/db.php
T
David DyerandClaude Sonnet 5 f1bbf59548 Add online menu management, modernized and hardened
Adds a public menu page (menu.php) and an admin CRUD interface
(admin/menu.php + admin/menu.js) backed by new menu_categories and
menu_items tables, replacing the "menu lives on Facebook" placeholder
on the homepage.

The admin JS/PHP came out of an AI-assisted pass that used outdated
JS (var, .then() chains, string-built HTML) and had some rough edges
security-wise, so this also:

- Rewrites the admin JS to modern ES (const/let, async/await,
  template literals, dataset API) and splits it out of admin/menu.php
  into admin/menu.js for proper separation of concerns
- Adds declare(strict_types=1) and PHP 8 match-based action dispatch
  in place of an if-chain
- Validates category_id/item_id/category_id server-side before use,
  so a bad ID returns a clean JSON error instead of an uncaught
  PDOException or a null item breaking the client
- Rejects non-string POST fields instead of crashing trim() on them
- Replaces the category-save handler's full-page location.reload()
  with in-place DOM updates (panel insert/update, resort by
  sort_order, keep the item modal's category dropdown in sync), so
  every admin action is now AJAX with no reload
- Sets data-name/data-desc via dataset assignment rather than string-
  interpolating them into an HTML attribute, closing a stored-XSS
  path through the category name/description fields

Removes seed-menu.php, a leftover stub with no working code left in
it after the AI's one-time seeding run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 21:35:22 -04:00

91 lines
2.9 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;
}
$dbDir = __DIR__ . '/../db';
if (!is_dir($dbDir)) {
mkdir($dbDir, 0755, true);
}
$dbPath = $dbDir . '/fitz.sqlite';
$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',
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
);
");
// Seed default admin user if table is empty (username: admin / password: password)
$stmt = $pdo->query("SELECT COUNT(*) as count FROM users");
$userCount = $stmt->fetchColumn();
if ($userCount == 0) {
$defaultPassHash = password_hash('password', PASSWORD_DEFAULT);
$insert = $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin')");
$insert->execute(['admin', $defaultPassHash]);
}
}