ipx-web: the page in its own container, behind Caddy (#203)
web/build.mjs builds a static site, web/dist, with the icons and typeface beside the pages; Dockerfile.web serves it with Caddy, which passes /api and /media to ipx on the same origin, so cookies, live updates and Cloudflare Access work as before. ipx serves no page any more: build.rs and every include_str! of the web files are gone, and its image is built without node. What ipx did to the page as it served it is kept another way. Caddy asks ipx before serving a page (forward_auth): /api/page-check sends a browser not signed in to /login and a non-admin away from /admin, and refuses them /admin.js; /api/login-check sends someone Access vouches for past the form; a page asked for with ?token= goes to /api/signin, which sets the cookie. The theme and the admin link, once written into the page per person, are set by web/boot.js in <head> from the theme cookie and the last visit, before the first frame, and /api/me brings the version and the account's theme from before. Caddy strips the Cf-Access-* headers from every peer but TUNNEL_PEER, the address cloudflared's requests come from, which is the boundary trusted_proxies drew; ipx then trusts only Caddy's address. The browser suite runs Caddy in front of its daemon, so it tests what production would run. Not deployed: production's cutover is on #203. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
// Builds the pages ipx serves: each page's TypeScript from web/src, types stripped and minified
|
||||
// by swc into a script of its own (app.js, login.js), and the page minified, its
|
||||
// <script data-src> pointing at that script. build.rs runs it into OUT_DIR, where web.rs
|
||||
// include_str!s the results, so the binary still carries everything and nothing is served
|
||||
// from disk.
|
||||
// Builds the frontend (#203): each page's TypeScript from web/src, types stripped and minified
|
||||
// by swc into a script of its own (app.js, login.js), the page minified, its <script data-src>
|
||||
// pointing at that script, and the icons, logos and typeface beside them. The result, web/dist,
|
||||
// is a static site: Caddy serves it (web/Caddyfile, Dockerfile.web) and passes the API to ipx.
|
||||
//
|
||||
// The page names its script with a hash of the script's contents, /app.js?v=<hash>, and the
|
||||
// server lets a browser keep that for a year without asking again. A changed script is a new
|
||||
// URL, and the page, which the browser checks on every visit, is what carries it.
|
||||
// URL, and the page, which the browser checks on every visit, is what carries it. Caddy's file
|
||||
// server ignores the query.
|
||||
//
|
||||
// node web/build.mjs [out-dir] default out-dir: web/dist
|
||||
import crypto from 'node:crypto';
|
||||
@@ -81,7 +81,7 @@ export function buildPage(name, { minify = true } = {}) {
|
||||
// The icons are named by their contents too. They are kept a day under a fixed name, and a new
|
||||
// logo went unseen for that day, in browsers and at Cloudflare's edge. The server ignores the
|
||||
// query; /favicon.ico, which a browser asks for on its own, cannot carry one.
|
||||
out = out.replace(/(href|src|srcset|data-light|data-dark)="\/(favicon\.png|favicon-dark\.png|apple-touch-icon\.png|logo\.svg|logo-dark\.svg)"/g,
|
||||
out = out.replace(/(href|src|srcset|data-light|data-dark)="\/(favicon\.png|favicon-dark\.png|apple-touch-icon\.png|logo\.svg|logo-dark\.svg|boot\.js)"/g,
|
||||
(_, attr, file) => `${attr}="/${file}?v=${hash(fs.readFileSync(path.join(here, file)))}"`);
|
||||
if (!minify) return { html: out, js, script };
|
||||
const r = html.minifySync(out, { minifyJs: false, minifyCss: true, removeComments: true });
|
||||
@@ -95,6 +95,16 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
fs.writeFileSync(path.join(out, STYLE), buildStyle());
|
||||
fs.writeFileSync(path.join(out, 'hls.js'), HLS);
|
||||
// Served as they are, under the names the pages and the stylesheet use. /favicon.ico is the
|
||||
// PNG, for a browser that asks for that on its own; iOS asks for the precomposed touch icon
|
||||
// first when the site is added to a home screen (#109). /icon.png is the 2004 icon, which
|
||||
// nothing here shows any more but the classic theme.
|
||||
for (const [from, to] of [
|
||||
['logo.svg', 'logo.svg'], ['logo-dark.svg', 'logo-dark.svg'],
|
||||
['favicon.png', 'favicon.png'], ['favicon.png', 'favicon.ico'], ['favicon-dark.png', 'favicon-dark.png'],
|
||||
['apple-touch-icon.png', 'apple-touch-icon.png'], ['apple-touch-icon.png', 'apple-touch-icon-precomposed.png'],
|
||||
['ipodderx-icon.png', 'icon.png'], ['boot.js', 'boot.js'], ['InterVariable.woff2', 'inter.woff2'], ['Inter-LICENSE.txt', 'Inter-LICENSE.txt'],
|
||||
]) fs.copyFileSync(path.join(here, from), path.join(out, to));
|
||||
for (const name of Object.keys(PAGES)) {
|
||||
const { html, js, script } = buildPage(name);
|
||||
fs.writeFileSync(path.join(out, name), html);
|
||||
|
||||
Reference in New Issue
Block a user