Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:; manifest-src 'self'" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:; manifest-src 'self'" always;

root /usr/share/nginx/html;
index index.html;
Expand All @@ -120,6 +120,22 @@ server {
add_header Expires 0;
}

# Neither of these filenames carries a content hash, so without an explicit
# rule they fall through to the immutable 1-year block below and a deploy
# cannot dislodge the old copy from browser caches. env-config.js changes
# per deployment; app-init.js is the pre-paint bootstrap that CSP requires
# to be a file rather than an inline script. nginx.conf has the same pair.
location = /env-config.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}
location = /app-init.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}

# Static assets with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)\$ {
expires 1y;
Expand Down
22 changes: 5 additions & 17 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,14 @@
<title>NinerLog - Pilot Logbook</title>
<!-- Runtime environment variables (injected by docker-entrypoint.sh) -->
<script src="/env-config.js"></script>
<!-- Update document title if APP_NAME is configured -->
<script>
if (window.ENV && window.ENV.VITE_APP_NAME) {
document.title = window.ENV.VITE_APP_NAME + ' - Pilot Logbook';
}
</script>
<!-- Title + pre-paint theme bootstrap. Kept in a file rather than inline
so the CSP can drop 'unsafe-inline' from script-src. Must stay a
classic, non-deferred script: it has to run before first paint. -->
<script src="/app-init.js"></script>
</head>
<body>
<noscript>NinerLog requires JavaScript to run.</noscript>
<!-- Prevent flash of wrong theme -->
<script>
(function() {
try {
var stored = JSON.parse(localStorage.getItem('ninerlog-theme') || '{}');
var theme = stored.state && stored.state.theme;
var dark = theme === 'dark' || (theme !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (dark) document.documentElement.classList.add('dark');
} catch(e) {}
})();
</script>
<!-- Theme is applied from /app-init.js in <head>, before first paint. -->
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
12 changes: 11 additions & 1 deletion nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ http {
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:; manifest-src 'self'" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:; manifest-src 'self'" always;

# Service worker + its registration bootstrap. vite-plugin-pwa's
# generateSW mode emits sw.js and registerSW.js (not the
Expand Down Expand Up @@ -145,6 +145,16 @@ http {
add_header Expires 0;
}

# Pre-paint bootstrap — same reasoning as env-config.js. Its filename
# carries no content hash, so without this it would fall through to the
# immutable 1-year rule below and a deploy could not dislodge the old
# copy from browser caches.
location = /app-init.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}

# Static assets with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
Expand Down
37 changes: 37 additions & 0 deletions public/app-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Pre-render bootstrap. This lives in a file rather than an inline <script>
// in index.html so the Content-Security-Policy can drop 'unsafe-inline' from
// script-src — with 'unsafe-inline' present, any reflected or stored string
// that reaches the DOM as markup executes, which is the exact class of bug a
// CSP is there to stop.
//
// Loaded as a classic, render-blocking script from <head> (see index.html).
// It must not become a module or gain `defer`: the theme block below has to
// run before first paint, and a deferred script paints the wrong theme first
// and corrects it afterwards.
//
// Served no-cache (see the /app-init.js location in nginx.conf) because the
// filename is not content-hashed — a long-lived immutable cache entry would
// pin an old copy across deploys.

// Reflect the deployment's configured app name in the document title.
// window.ENV comes from /env-config.js, which is loaded immediately before
// this file.
if (window.ENV && window.ENV.VITE_APP_NAME) {
document.title = window.ENV.VITE_APP_NAME + ' - Pilot Logbook';
}

// Apply the persisted theme before the first paint, so a dark-theme user does
// not get a white flash while React boots. Mirrors the zustand-persist shape
// used by the theme store; a malformed or absent value falls through to the
// OS preference.
(function () {
try {
var stored = JSON.parse(localStorage.getItem('ninerlog-theme') || '{}');
var theme = stored.state && stored.state.theme;
var dark = theme === 'dark' || (theme !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (dark) document.documentElement.classList.add('dark');
} catch (e) {
// localStorage can throw (Safari private browsing, disabled storage) —
// the OS preference still applies via CSS, so there is nothing to do.
}
})();
Loading