Refactor admin pages to use shared head and navigation components
- Created `admin-head.php` for consistent <head> markup across admin pages. - Created `admin-nav.php` for shared navigation bar, reducing duplication. - Updated all admin pages (account, hours, index, login, menu, users) to utilize the new components. - Added security headers to `auth.php` and `index.php` for improved security. - Introduced `security-headers.php` to manage security headers across the application. - Moved inline styles from admin pages to SCSS files to comply with Content Security Policy (CSP). - Normalized file permissions during build process to prevent permission issues on deployment.
This commit is contained in:
1 parent
3286bce6b1
commit
c7b836bcf2
21 files changed
+447
-231
No files matched your search
+4
-31
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/assets.php';
|
||||
require_once __DIR__ . '/../inc/admin-head.php';
|
||||
require_once __DIR__ . '/../inc/admin-nav.php';
|
||||
require_once __DIR__ . '/../inc/users.php';
|
||||
|
||||
// Any signed-in user can change their own username and/or password here,
|
||||
@@ -80,46 +82,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
$justSaved = !$_POST && isset($_GET['saved']);
|
||||
$isAdmin = ($user['role'] ?? '') === 'admin';
|
||||
// After a failed save, keep the username they typed in the field.
|
||||
$usernameValue = $_POST ? trim(account_post_str('username')) : $user['username'];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>My Account | The Old Fitz Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
||||
</head>
|
||||
<?php render_admin_head('My Account'); ?>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php">Announcements</a>
|
||||
<a href="menu.php">Menu</a>
|
||||
<?php if ($isAdmin): ?>
|
||||
<a href="users.php">Users</a>
|
||||
<?php endif; ?>
|
||||
<a href="hours.php">Hours</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="account.php" class="admin-account-link active" title="My Account"><?= htmlspecialchars($user['username']) ?></a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<?php render_admin_nav($user, 'account'); ?>
|
||||
|
||||
<main class="dashboard-layout">
|
||||
<div class="container">
|
||||
|
||||
+6
-32
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/assets.php';
|
||||
require_once __DIR__ . '/../inc/admin-head.php';
|
||||
require_once __DIR__ . '/../inc/admin-nav.php';
|
||||
require_once __DIR__ . '/../inc/hours.php';
|
||||
|
||||
require_auth();
|
||||
@@ -66,39 +68,11 @@ $justSaved = !$_POST && isset($_GET['saved']);
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Hours | The Old Fitz Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
||||
</head>
|
||||
<?php render_admin_head('Hours'); ?>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php">Announcements</a>
|
||||
<a href="menu.php">Menu</a>
|
||||
<?php if (($user['role'] ?? '') === 'admin'): ?>
|
||||
<a href="users.php">Users</a>
|
||||
<?php endif; ?>
|
||||
<a href="hours.php" class="active">Hours</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="account.php" class="admin-account-link" title="My Account"><?= htmlspecialchars($user['username']) ?></a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<?php render_admin_nav($user, 'hours'); ?>
|
||||
|
||||
<main class="dashboard-layout">
|
||||
<div class="container">
|
||||
@@ -157,7 +131,7 @@ $justSaved = !$_POST && isset($_GET['saved']);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="muted field-hint" style="margin-top: 1.25rem;">Leave "Kitchen Closes" blank on a day if the kitchen doesn't have a separate cutoff from the bar. Special closures (holidays, weather) are handled on the <a href="index.php">Announcements</a> page instead of here.</p>
|
||||
<p class="muted field-hint hours-note">Leave "Kitchen Closes" blank on a day if the kitchen doesn't have a separate cutoff from the bar. Special closures (holidays, weather) are handled on the <a href="index.php">Announcements</a> page instead of here.</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Hours</button>
|
||||
@@ -171,7 +145,7 @@ $justSaved = !$_POST && isset($_GET['saved']);
|
||||
<span>👀 Public Site Preview</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p class="muted" style="margin-bottom: 1.25rem;">This is how the schedule above will read on the site once saved.</p>
|
||||
<p class="muted hours-preview-intro">This is how the schedule above will read on the site once saved.</p>
|
||||
|
||||
<div class="hours-preview">
|
||||
<div class="hours-preview-card">
|
||||
|
||||
+10
-36
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/assets.php';
|
||||
require_once __DIR__ . '/../inc/admin-head.php';
|
||||
require_once __DIR__ . '/../inc/admin-nav.php';
|
||||
require_once __DIR__ . '/../inc/announcements.php';
|
||||
|
||||
require_auth();
|
||||
@@ -102,38 +104,10 @@ $announcements = get_all_announcements();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pub Management Dashboard | The Old Fitz</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
||||
</head>
|
||||
<?php render_admin_head('Announcements'); ?>
|
||||
<body>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php" class="active">Announcements</a>
|
||||
<a href="menu.php">Menu</a>
|
||||
<?php if (($user['role'] ?? '') === 'admin'): ?>
|
||||
<a href="users.php">Users</a>
|
||||
<?php endif; ?>
|
||||
<a href="hours.php">Hours</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="account.php" class="admin-account-link" title="My Account"><?= htmlspecialchars($user['username']) ?></a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<?php render_admin_nav($user, 'announcements'); ?>
|
||||
|
||||
<main class="dashboard-layout">
|
||||
<div class="container">
|
||||
@@ -194,21 +168,21 @@ $announcements = get_all_announcements();
|
||||
</div>
|
||||
<div data-label="Headline">
|
||||
<strong><?= htmlspecialchars($ann['headline']) ?></strong>
|
||||
<div class="muted" style="font-size: 0.82rem; margin-top: 0.2rem; max-width: 32ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<div class="muted ann-summary">
|
||||
<?= htmlspecialchars($ann['message']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div data-label="Type">
|
||||
<span class="eyebrow"><?= htmlspecialchars($ann['type']) ?></span>
|
||||
</div>
|
||||
<div class="font-mono" data-label="Schedule" style="font-size: 0.8rem; color: var(--ink-muted);">
|
||||
<div class="font-mono ann-schedule" data-label="Schedule">
|
||||
<?php if (!empty($ann['start_date']) || !empty($ann['end_date'])): ?>
|
||||
<?= htmlspecialchars($ann['start_date'] ?: 'Now') ?> → <?= htmlspecialchars($ann['end_date'] ?: 'Ongoing') ?>
|
||||
<?php else: ?>
|
||||
Always (while on)
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div data-label="Placements" style="font-size: 0.82rem; color: var(--ink-muted);">
|
||||
<div class="ann-placements" data-label="Placements">
|
||||
<?php
|
||||
$locs = [];
|
||||
if (!empty($ann['show_banner'])) $locs[] = 'Top Banner';
|
||||
@@ -221,7 +195,7 @@ $announcements = get_all_announcements();
|
||||
<div class="action-stack">
|
||||
<a href="index.php?edit=<?= (int)$ann['id'] ?>" class="btn btn-outline btn-sm">Edit</a>
|
||||
|
||||
<form method="POST" action="index.php" style="display: inline;" class="js-confirm-submit" data-confirm="Are you sure you want to delete this announcement?">
|
||||
<form method="POST" action="index.php" class="inline-form js-confirm-submit" data-confirm="Are you sure you want to delete this announcement?">
|
||||
<input type="hidden" name="action" value="delete_announcement">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||
<input type="hidden" name="announcement_id" value="<?= (int)$ann['id'] ?>">
|
||||
@@ -286,14 +260,14 @@ $announcements = get_all_announcements();
|
||||
<label for="start_date">Start Date (Optional Auto-Schedule)</label>
|
||||
<input type="date" id="start_date" name="start_date" class="form-control"
|
||||
value="<?= htmlspecialchars($editItem['start_date'] ?? '') ?>">
|
||||
<span class="muted font-mono" style="font-size: 0.78rem;">Leave blank to show immediately while enabled.</span>
|
||||
<span class="muted font-mono ann-field-hint">Leave blank to show immediately while enabled.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="end_date">End Date (Optional Expiration)</label>
|
||||
<input type="date" id="end_date" name="end_date" class="form-control"
|
||||
value="<?= htmlspecialchars($editItem['end_date'] ?? '') ?>">
|
||||
<span class="muted font-mono" style="font-size: 0.78rem;">Auto-hides after 11:59 PM on this date.</span>
|
||||
<span class="muted font-mono ann-field-hint">Auto-hides after 11:59 PM on this date.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
|
||||
+2
-9
@@ -2,6 +2,7 @@
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/login-throttle.php';
|
||||
require_once __DIR__ . '/../inc/assets.php';
|
||||
require_once __DIR__ . '/../inc/admin-head.php';
|
||||
|
||||
$error = '';
|
||||
|
||||
@@ -27,15 +28,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Admin Login | The Old Fitz Public House</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@600&family=Karla:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
||||
</head>
|
||||
<?php render_admin_head('Sign In'); ?>
|
||||
<body class="admin-login-body">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
|
||||
+9
-3
@@ -71,10 +71,16 @@ function closeCategoryModal() {
|
||||
catModal.close();
|
||||
}
|
||||
|
||||
// Escapes for both text and quoted-attribute positions. The old version
|
||||
// (textContent → innerHTML) didn't escape quotes, so a name like
|
||||
// The "Big" Burger broke out of an aria-label="..." attribute.
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text ?? '';
|
||||
return div.innerHTML;
|
||||
return String(text ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
async function postForm(formData) {
|
||||
|
||||
+4
-30
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/assets.php';
|
||||
require_once __DIR__ . '/../inc/admin-head.php';
|
||||
require_once __DIR__ . '/../inc/admin-nav.php';
|
||||
require_once __DIR__ . '/../inc/menu.php';
|
||||
|
||||
require_auth();
|
||||
@@ -180,39 +182,11 @@ $menuEnabled = is_menu_enabled();
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Menu Management | The Old Fitz Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
||||
</head>
|
||||
<?php render_admin_head('Menu'); ?>
|
||||
|
||||
<body data-csrf-token="<?= htmlspecialchars(get_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
|
||||
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php">Announcements</a>
|
||||
<a href="menu.php" class="active">Menu</a>
|
||||
<?php if (($user['role'] ?? '') === 'admin'): ?>
|
||||
<a href="users.php">Users</a>
|
||||
<?php endif; ?>
|
||||
<a href="hours.php">Hours</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="account.php" class="admin-account-link" title="My Account"><?= htmlspecialchars($user['username']) ?></a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<?php render_admin_nav($user, 'menu'); ?>
|
||||
|
||||
<main class="dashboard-layout">
|
||||
<div class="container">
|
||||
|
||||
+9
-3
@@ -9,10 +9,16 @@ const userModal = document.getElementById('modal-user');
|
||||
const passwordModal = document.getElementById('modal-password');
|
||||
const editModal = document.getElementById('modal-edit');
|
||||
|
||||
// Escapes for both text and quoted-attribute positions. The old version
|
||||
// (textContent → innerHTML) didn't escape quotes, so a name like
|
||||
// The "Big" Burger broke out of an aria-label="..." attribute.
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text ?? '';
|
||||
return div.innerHTML;
|
||||
return String(text ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
async function postForm(formData) {
|
||||
|
||||
+4
-28
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/assets.php';
|
||||
require_once __DIR__ . '/../inc/admin-head.php';
|
||||
require_once __DIR__ . '/../inc/admin-nav.php';
|
||||
require_once __DIR__ . '/../inc/users.php';
|
||||
|
||||
// User management is admin-only. Other pages don't check roles yet —
|
||||
@@ -190,37 +192,11 @@ $users = get_all_users();
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>User Management | The Old Fitz Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
||||
</head>
|
||||
<?php render_admin_head('Users'); ?>
|
||||
|
||||
<body data-csrf-token="<?= htmlspecialchars(get_csrf_token(), ENT_QUOTES, 'UTF-8') ?>" data-current-user-id="<?= (int) $user['id'] ?>">
|
||||
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php">Announcements</a>
|
||||
<a href="menu.php">Menu</a>
|
||||
<a href="users.php" class="active">Users</a>
|
||||
<a href="hours.php">Hours</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="account.php" class="admin-account-link" title="My Account"><?= htmlspecialchars($user['username']) ?></a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<?php render_admin_nav($user, 'users'); ?>
|
||||
|
||||
<main class="dashboard-layout">
|
||||
<div class="container">
|
||||
|
||||
+39
-33
@@ -1,52 +1,58 @@
|
||||
# The Old Fitz — production compose for the VPS.
|
||||
#
|
||||
# Expected layout, one folder per site:
|
||||
# theoldfitz/
|
||||
# docker-compose.yaml (this file)
|
||||
# .env (optional, see PROXY_NETWORK below)
|
||||
# www/ (contents of `npm run dist`)
|
||||
# data/ (fitz.sqlite — never inside www/)
|
||||
#
|
||||
# First run: create data/ and `chown 33:33 data` (www-data in the image),
|
||||
# then create the first admin with bin/create-user.php (see its header).
|
||||
#
|
||||
# The CasaOS dev host keeps its own copy of this file in the CasaOS app
|
||||
# settings, with CasaOS paths and its x-casaos block.
|
||||
|
||||
name: the-old-fitz
|
||||
|
||||
networks:
|
||||
nginxproxymanager:
|
||||
name: nginxproxymanager
|
||||
proxy:
|
||||
external: true
|
||||
# The reverse proxy's Docker network (NPM or Caddy). Set PROXY_NETWORK
|
||||
# in .env if it isn't called "proxy".
|
||||
name: ${PROXY_NETWORK:-proxy}
|
||||
|
||||
services:
|
||||
php-server:
|
||||
the-old-fitz:
|
||||
image: php:8.5-apache
|
||||
container_name: the-old-fitz
|
||||
restart: unless-stopped
|
||||
# The image ships without a php.ini, so PHP falls back to dev-style
|
||||
# defaults and shows errors (with server paths) to visitors. Activate
|
||||
# the image's bundled production ini — display_errors off, errors to
|
||||
# the log — and hide the PHP version header, then start Apache as usual.
|
||||
# Paths are spelled out (the image's $PHP_INI_DIR) rather than using
|
||||
# the variable, so no $-escaping is needed.
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && echo 'expose_php = Off' > /usr/local/etc/php/conf.d/zz-hardening.ini && exec apache2-foreground
|
||||
networks:
|
||||
- nginxproxymanager
|
||||
- proxy
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /DATA/AppData/theoldfitz/www
|
||||
source: ./www
|
||||
target: /var/www/html
|
||||
# Database lives outside the web root so Apache can't serve it.
|
||||
# FITZ_DATA_DIR defaults to the CasaOS dev host's path; set it in a
|
||||
# .env file next to this compose file on the VPS.
|
||||
- type: bind
|
||||
source: ${FITZ_DATA_DIR:-/DATA/AppData/theoldfitz/data}
|
||||
source: ./data
|
||||
target: /var/www/data
|
||||
environment:
|
||||
- TZ=America/Indiana/Indianapolis
|
||||
- FITZ_DB_PATH=/var/www/data/fitz.sqlite
|
||||
labels:
|
||||
icon: https://www.php.net/images/logos/php-logo.svg
|
||||
|
||||
x-casaos:
|
||||
architectures:
|
||||
- amd64
|
||||
- arm64
|
||||
author: self
|
||||
category: Developer
|
||||
description:
|
||||
en_us: PHP web server running Apache, proxied through Nginx Proxy Manager.
|
||||
developer: PHP
|
||||
hostname: theoldfitz.daviddyer.me
|
||||
icon: https://www.php.net/images/logos/php-logo.svg
|
||||
id: self.php-server
|
||||
index: /
|
||||
is_uncontrolled: false
|
||||
main: php-server
|
||||
port_map: "80"
|
||||
scheme: http
|
||||
tagline:
|
||||
en_us: PHP 8.5 + Apache web server.
|
||||
thumbnail: https://www.php.net/images/logos/php-logo.svg
|
||||
title:
|
||||
en_us: PHP Server
|
||||
# Cap container logs so PHP's error log can't slowly fill the disk.
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/assets.php';
|
||||
|
||||
/**
|
||||
* Shared <head> for every admin page. $title is the page's own name; the
|
||||
* site suffix is added here so every tab reads "<Page> | The Old Fitz Admin".
|
||||
*/
|
||||
function render_admin_head(string $title): void {
|
||||
?>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><?= htmlspecialchars($title) ?> | The Old Fitz Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
||||
</head>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Shared admin navigation bar. Every admin page calls
|
||||
* render_admin_nav($user, '<key>') where it used to repeat the markup.
|
||||
*
|
||||
* $active is one of the ADMIN_NAV_LINKS keys, or 'account' for the
|
||||
* My Account page (which highlights the username link instead).
|
||||
*/
|
||||
|
||||
const ADMIN_NAV_LINKS = [
|
||||
// key => [href, label, admin-only]
|
||||
'announcements' => ['index.php', 'Announcements', false],
|
||||
'menu' => ['menu.php', 'Menu', false],
|
||||
'users' => ['users.php', 'Users', true],
|
||||
'hours' => ['hours.php', 'Hours', false],
|
||||
];
|
||||
|
||||
function render_admin_nav(array $user, string $active): void {
|
||||
$isAdmin = ($user['role'] ?? '') === 'admin';
|
||||
$current = static fn (string $key): string =>
|
||||
$key === $active ? ' class="active" aria-current="page"' : '';
|
||||
?>
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-nav-links">
|
||||
<?php foreach (ADMIN_NAV_LINKS as $key => [$href, $label, $adminOnly]): ?>
|
||||
<?php if (!$adminOnly || $isAdmin): ?>
|
||||
<a href="<?= $href ?>"<?= $current($key) ?>><?= $label ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="account.php" class="admin-account-link<?= $active === 'account' ? ' active' : '' ?>" title="My Account"<?= $active === 'account' ? ' aria-current="page"' : '' ?>><?= htmlspecialchars($user['username']) ?></a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<?php
|
||||
}
|
||||
+2
-15
@@ -3,21 +3,8 @@
|
||||
* Auth & session helper
|
||||
*/
|
||||
|
||||
/**
|
||||
* Whether the visitor reached us over HTTPS. TLS ends at the reverse proxy
|
||||
* (NPM or Caddy), so PHP itself only ever sees plain HTTP — trust the
|
||||
* proxy's X-Forwarded-Proto, but only from a private/loopback address so a
|
||||
* public client can't claim it.
|
||||
*/
|
||||
function request_is_https(): bool {
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
return true;
|
||||
}
|
||||
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$fromProxy = filter_var($remote, FILTER_VALIDATE_IP)
|
||||
&& !filter_var($remote, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
||||
return $fromProxy && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
|
||||
}
|
||||
require_once __DIR__ . '/security-headers.php';
|
||||
send_security_headers();
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Configure secure session cookie defaults. 'secure' follows the
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Security headers for every page, sent from PHP so they apply the same
|
||||
* under NPM, Caddy, or local dev. Public pages call send_security_headers()
|
||||
* directly; admin pages get it through inc/auth.php.
|
||||
*
|
||||
* Static files (CSS, JS, images) are served by Apache and don't pass
|
||||
* through here — that's fine, since the CSP only matters on documents.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Whether the visitor reached us over HTTPS. TLS ends at the reverse proxy
|
||||
* (NPM or Caddy), so PHP itself only ever sees plain HTTP — trust the
|
||||
* proxy's X-Forwarded-Proto, but only from a private/loopback address so a
|
||||
* public client can't claim it.
|
||||
*/
|
||||
function request_is_https(): bool {
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
return true;
|
||||
}
|
||||
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$fromProxy = filter_var($remote, FILTER_VALIDATE_IP)
|
||||
&& !filter_var($remote, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
||||
return $fromProxy && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
|
||||
}
|
||||
|
||||
function send_security_headers(): void {
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Under `npm run dev`, browser-sync injects an inline <script> to load
|
||||
// its live-reload client and talks over a websocket. Allow exactly that
|
||||
// in dev only; production stays strict. Inline *styles* are blocked in
|
||||
// dev too, so a stray style="" still shows up while developing.
|
||||
$isDev = PHP_SAPI === 'cli-server';
|
||||
|
||||
$csp = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self'" . ($isDev ? " 'unsafe-inline'" : ''),
|
||||
"style-src 'self' https://fonts.googleapis.com",
|
||||
"font-src https://fonts.gstatic.com",
|
||||
"img-src 'self'",
|
||||
"connect-src 'self'" . ($isDev ? ' ws: wss:' : ''),
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
];
|
||||
|
||||
header('Content-Security-Policy: ' . implode('; ', $csp));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
|
||||
|
||||
// HSTS only over HTTPS (browsers ignore it on HTTP anyway). No
|
||||
// includeSubDomains: this site is one subdomain of daviddyer.me and
|
||||
// shouldn't dictate policy for the others.
|
||||
if (request_is_https()) {
|
||||
header('Strict-Transport-Security: max-age=31536000');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/inc/assets.php';
|
||||
require __DIR__ . '/inc/security-headers.php';
|
||||
send_security_headers();
|
||||
require __DIR__ . '/inc/google-reviews.php';
|
||||
require __DIR__ . '/inc/announcements.php';
|
||||
require __DIR__ . '/inc/hours.php';
|
||||
|
||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/inc/assets.php';
|
||||
require __DIR__ . '/inc/security-headers.php';
|
||||
send_security_headers();
|
||||
require __DIR__ . '/inc/announcements.php';
|
||||
require __DIR__ . '/inc/menu.php';
|
||||
require __DIR__ . '/inc/hours.php';
|
||||
@@ -94,24 +96,24 @@ $siteUrl = rtrim(SITE_URL, '/');
|
||||
<main id="main">
|
||||
|
||||
<section class="menu-page-header">
|
||||
<div class="container" style="text-align: center;">
|
||||
<div class="container menu-hero">
|
||||
<p class="eyebrow">The Old Fitz Public House</p>
|
||||
<h1 style="font-size: clamp(2.4rem, 5vw, 3.8rem); color: var(--gold-bright); margin-top: 0.5rem;">Pub Menu</h1>
|
||||
<div class="rule-row" style="margin-top: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<h1 class="menu-hero-title">Pub Menu</h1>
|
||||
<div class="rule-row menu-hero-rule">
|
||||
<span class="line"></span>
|
||||
<img class="knot" src="./assets/knot.png" alt="">
|
||||
<span class="line"></span>
|
||||
</div>
|
||||
<p class="muted" style="max-width: 52ch; margin: 0 auto; font-size: 1.05rem;">
|
||||
<p class="muted menu-hero-intro">
|
||||
Traditional Irish staples, shareable plates, and comforting pub classics cooked fresh. Kitchen serves until 9:00 PM.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="menu-page-content" style="padding-top: 0;">
|
||||
<div class="container" style="max-width: 62rem;">
|
||||
<section class="menu-page-content">
|
||||
<div class="container menu-page-container">
|
||||
<?php if ($menuEnabled && !empty($menuCategories)): ?>
|
||||
<div class="menu-grid" style="margin-top: 0;">
|
||||
<div class="menu-grid menu-grid-flush">
|
||||
<?php foreach ($menuCategories as $category): ?>
|
||||
<?php if (!empty($category['items'])): ?>
|
||||
<div class="menu-category">
|
||||
@@ -144,17 +146,17 @@ $siteUrl = rtrim(SITE_URL, '/');
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="panel" style="text-align: center; padding: 3rem 1.5rem;">
|
||||
<div class="panel menu-empty">
|
||||
<p class="muted">Our kitchen menu is currently being refreshed. Please check our Facebook page for today's specials!</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Allergen / Food Safety Disclaimer from the original print menu -->
|
||||
<div class="menu-disclaimer" style="margin-top: 3.5rem; text-align: center; padding-top: 2rem; border-top: 1px solid var(--rule);">
|
||||
<p class="muted" style="font-size: 0.8rem; line-height: 1.6; max-width: 68ch; margin: 0 auto;">
|
||||
<div class="menu-disclaimer">
|
||||
<p class="muted menu-disclaimer-text">
|
||||
Consuming raw or undercooked meats, poultry, seafood, shellfish, or eggs may increase your risk of foodborne illness. Our kitchen handles a variety of ingredients, and our menu items may contain or come into contact with common allergens such as milk, eggs, wheat, soy, nuts, or shellfish.
|
||||
</p>
|
||||
<div class="hero-ctas" style="margin-top: 2rem; justify-content: center;">
|
||||
<div class="hero-ctas menu-disclaimer-ctas">
|
||||
<a class="btn btn-primary" href="https://www.facebook.com/profile.php?id=100086635646712" target="_blank" rel="noopener">See Daily Specials on Facebook</a>
|
||||
<a class="btn btn-outline" href="mailto:info@theoldfitzpub.com">Ask a Kitchen Question</a>
|
||||
</div>
|
||||
|
||||
@@ -74,6 +74,25 @@ if (fs.existsSync(dbHtaccess)) {
|
||||
missing.push('db/.htaccess');
|
||||
}
|
||||
|
||||
// Normalize permissions: 755 for directories, 644 for files. Copies keep
|
||||
// the source file's mode, so a file that's owner-only on the dev machine
|
||||
// (e.g. after an in-place edit) would ship unreadable to Apache's
|
||||
// www-data and 500 the page. Forcing the modes here means local
|
||||
// permissions can never break a deploy.
|
||||
function normalizePermissions(dir) {
|
||||
fs.chmodSync(dir, 0o755);
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
normalizePermissions(full);
|
||||
} else {
|
||||
fs.chmodSync(full, 0o644);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalizePermissions(DIST);
|
||||
|
||||
if (missing.length) {
|
||||
console.warn('Warning: expected but not found, skipped:\n ' + missing.join('\n '));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/* ---------- admin: announcements ---------- */
|
||||
/* Formerly inline style="" attributes on admin/index.php — moved here so
|
||||
the CSP can forbid inline styles. */
|
||||
.ann-summary {
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.2rem;
|
||||
max-width: 32ch;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ann-schedule {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.ann-placements {
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.ann-field-hint {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline;
|
||||
}
|
||||
@@ -141,3 +141,12 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Formerly inline style="" attributes on admin/hours.php. */
|
||||
.hours-note {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.hours-preview-intro {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
@@ -94,5 +94,61 @@
|
||||
}
|
||||
|
||||
.menu-page-content {
|
||||
padding-top: 0;
|
||||
padding-bottom: 5.5rem;
|
||||
}
|
||||
|
||||
/* Formerly inline style="" attributes on menu.php — moved here so the
|
||||
CSP can forbid inline styles. */
|
||||
.menu-hero {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-hero-title {
|
||||
font-size: clamp(2.4rem, 5vw, 3.8rem);
|
||||
color: var(--gold-bright);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.menu-hero-rule {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.menu-hero-intro {
|
||||
max-width: 52ch;
|
||||
margin: 0 auto;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.menu-page-container {
|
||||
max-width: 62rem;
|
||||
}
|
||||
|
||||
.menu-grid-flush {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.menu-empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.menu-disclaimer {
|
||||
margin-top: 3.5rem;
|
||||
text-align: center;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.menu-disclaimer-text {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
max-width: 68ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.menu-disclaimer-ctas {
|
||||
margin-top: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -31,4 +31,5 @@
|
||||
@use "pages/admin-login";
|
||||
@use "pages/admin-menu";
|
||||
@use "pages/admin-hours";
|
||||
@use "pages/admin-announcements";
|
||||
@use "pages/admin-account";
|
||||
+95
@@ -1933,9 +1933,65 @@ footer a.email:hover {
|
||||
}
|
||||
|
||||
.menu-page-content {
|
||||
padding-top: 0;
|
||||
padding-bottom: 5.5rem;
|
||||
}
|
||||
|
||||
/* Formerly inline style="" attributes on menu.php — moved here so the
|
||||
CSP can forbid inline styles. */
|
||||
.menu-hero {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-hero-title {
|
||||
font-size: clamp(2.4rem, 5vw, 3.8rem);
|
||||
color: var(--gold-bright);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.menu-hero-rule {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.menu-hero-intro {
|
||||
max-width: 52ch;
|
||||
margin: 0 auto;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.menu-page-container {
|
||||
max-width: 62rem;
|
||||
}
|
||||
|
||||
.menu-grid-flush {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.menu-empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.menu-disclaimer {
|
||||
margin-top: 3.5rem;
|
||||
text-align: center;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.menu-disclaimer-text {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
max-width: 68ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.menu-disclaimer-ctas {
|
||||
margin-top: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ---------- admin / management styles ---------- */
|
||||
.admin-login-body {
|
||||
min-height: 100vh;
|
||||
@@ -2196,6 +2252,45 @@ footer a.email:hover {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
/* Formerly inline style="" attributes on admin/hours.php. */
|
||||
.hours-note {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.hours-preview-intro {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
/* ---------- admin: announcements ---------- */
|
||||
/* Formerly inline style="" attributes on admin/index.php — moved here so
|
||||
the CSP can forbid inline styles. */
|
||||
.ann-summary {
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.2rem;
|
||||
max-width: 32ch;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ann-schedule {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.ann-placements {
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.ann-field-hint {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* ---------- admin: my account ---------- */
|
||||
/* One panel, fields in the shared two-column .form-grid (which already
|
||||
stacks below 47.5em). Capped so the fields don't stretch across the
|
||||
|
||||
Reference in new issue
Block a user