diff --git a/apps/pwa/package-lock.json b/apps/pwa/package-lock.json index e2ad17bc..9d18f13e 100644 --- a/apps/pwa/package-lock.json +++ b/apps/pwa/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@libsql/client": "^0.14.0", "@peculiar/x509": "^1.14.3", + "@profullstack/footer": "0.2.6", "@profullstack/libsql-pg": "^0.1.2", "@profullstack/notifications": "^0.1.0", "@profullstack/synconfig": "^0.1.1", @@ -396,6 +397,26 @@ "node": ">=20.0.0" } }, + "node_modules/@profullstack/footer": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@profullstack/footer/-/footer-0.2.6.tgz", + "integrity": "sha512-BKwbDszAc7e0mYIBiPjgr/5WS8WyDt8IW718s/zW/8pCWOIB1BGwSNs6fmImRSo5kqIjQKUVbXp22jZQ5GEipA==", + "license": "MIT", + "bin": { + "profullstack-footer": "bin/profullstack-footer.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": ">=18" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@profullstack/libsql-pg": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@profullstack/libsql-pg/-/libsql-pg-0.1.2.tgz", diff --git a/apps/pwa/package.json b/apps/pwa/package.json index 87cfb74d..119c2f62 100644 --- a/apps/pwa/package.json +++ b/apps/pwa/package.json @@ -16,6 +16,7 @@ "dependencies": { "@libsql/client": "^0.14.0", "@peculiar/x509": "^1.14.3", + "@profullstack/footer": "0.2.6", "@profullstack/libsql-pg": "^0.1.2", "@profullstack/notifications": "^0.1.0", "@profullstack/synconfig": "^0.1.1", diff --git a/apps/pwa/pnpm-lock.yaml b/apps/pwa/pnpm-lock.yaml index 9b8f0a6b..c908a508 100644 --- a/apps/pwa/pnpm-lock.yaml +++ b/apps/pwa/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@peculiar/x509': specifier: ^1.14.3 version: 1.14.3 + '@profullstack/footer': + specifier: 0.2.6 + version: 0.2.6 '@profullstack/libsql-pg': specifier: ^0.1.2 version: 0.1.2 @@ -215,6 +218,16 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} + '@profullstack/footer@0.2.6': + resolution: {integrity: sha512-BKwbDszAc7e0mYIBiPjgr/5WS8WyDt8IW718s/zW/8pCWOIB1BGwSNs6fmImRSo5kqIjQKUVbXp22jZQ5GEipA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + react: '>=18' + peerDependenciesMeta: + react: + optional: true + '@profullstack/libsql-pg@0.1.2': resolution: {integrity: sha512-6YrQujssWhkyWInB0G4CWOhH6Pj6kydeK4RKrXOosxQ12flpO4VjUEGTabtSh9zZ7FzCRch0hwFNKApJl/7b4w==} engines: {node: '>=20'} @@ -874,6 +887,8 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 + '@profullstack/footer@0.2.6': {} + '@profullstack/libsql-pg@0.1.2': dependencies: pg: 8.23.0 diff --git a/apps/pwa/src/lib/ring-footer.mjs b/apps/pwa/src/lib/ring-footer.mjs new file mode 100644 index 00000000..c57fe184 --- /dev/null +++ b/apps/pwa/src/lib/ring-footer.mjs @@ -0,0 +1,41 @@ +// The Profullstack footer (links, copyright, the << Profullstack >> webring row and +// the built.indie + pwamart badges) from @profullstack/footer, on the public +// sign-in page. It is rendered into the HTML on the server because rssamplifier's +// ring verifier reads the page and never runs script. +// +// Pages render synchronously, so the footer is kept as a string: the bundled +// template at boot, then the @latest template from the CDN, refreshed every 15 +// minutes (a footer release reaches us with no redeploy). A failed refresh keeps +// the last good copy. app.moshcode.sh is an ICANN host: clearnet template only. +import { footerHtml, footerHtmlSync } from "@profullstack/footer"; + +export const RING_FOOTER_OPTIONS = { + site: "https://app.moshcode.sh/", + links: [ + { label: "moshcode.sh", href: "https://moshcode.sh/" }, + { label: "The pit", href: "/pit" }, + ], +}; + +let html = footerHtmlSync(RING_FOOTER_OPTIONS); + +/** The footer markup; never empty. */ +export function ringFooter() { + return html; +} + +/** Accept a rendered footer only when it still links the ring. */ +export function setRingFooter(next) { + if (typeof next !== "string" || !next.includes("/ring/profullstack") || next.length > 64_000) return false; + html = next; + return true; +} + +export function startRingFooterRefresh(everyMs = 15 * 60_000) { + if (process.env.PROFULLSTACK_FOOTER_OFFLINE === "1") return () => {}; + const run = () => { footerHtml(RING_FOOTER_OPTIONS).then(setRingFooter, () => {}); }; + run(); + const timer = setInterval(run, everyMs); + timer.unref?.(); + return () => clearInterval(timer); +} diff --git a/apps/pwa/src/routes/auth.mjs b/apps/pwa/src/routes/auth.mjs index e2d5e145..e2739377 100644 --- a/apps/pwa/src/routes/auth.mjs +++ b/apps/pwa/src/routes/auth.mjs @@ -6,6 +6,7 @@ import { hashPassword, verifyPassword } from "../lib/crypto.mjs"; import { createUserWithPassword, userByEmail } from "../lib/users.mjs"; import { dashboardHandler } from "./pages.mjs"; import { config } from "../config.mjs"; +import { ringFooter } from "../lib/ring-footer.mjs"; export const authRouter = Router(); @@ -45,6 +46,7 @@ function authPage(req, { error = "", mode = "in" } = {}) {
${footer} +