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
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user