From 1222f8a1429eeb011d89f7528b5730594b8cb86a Mon Sep 17 00:00:00 2001 From: Levi Zitting Date: Sun, 4 Oct 2026 15:15:11 -0500 Subject: [PATCH] build: package the frontend Node runtime --- .dockerignore | 24 ++++++++ .env.example | 15 +++++ .github/workflows/ci.yml | 25 ++++++++ Dockerfile | 20 +++++++ README.md | 126 +++++++++++++++++++++++++++------------ vite.config.ts | 4 +- 6 files changed, 176 insertions(+), 38 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..be49fe86 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +# Only the inputs copied by Dockerfile belong in the build context. +** +!package.json +!package-lock.json +!.npmrc +!vite.config.ts +!tsconfig.json +!src/ +!src/** +!static/ +!static/** +!Dockerfile +!.dockerignore + +# Keep secrets and generated/local files out even inside source directories. +**/.env +**/.env.* +**/*.pem +**/*.key +**/secrets.json +**/node_modules +**/.svelte-kit +**/build +**/*.sqlite.db* diff --git a/.env.example b/.env.example index db2c736d..8b440ebf 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +# Vite loads ignored .env files. Built Node does not: inject these at startup +# or use node --env-file=.env build. Never pass private values as Docker build args. # Server-only. Used only when SvelteKit server code calls the CMS API. CMS_INTERNAL_ORIGIN=http://127.0.0.1:5099 @@ -7,6 +9,9 @@ CMS_INTERNAL_ORIGIN=http://127.0.0.1:5099 # Member login is disabled until CMS_MEMBER_BRIDGE_SECRET is set privately and matches # backend SGFDevs__MemberBridge__Secret. Never commit a secret here. +# CMS_MEMBER_BRIDGE_SECRET= +# Set backend SGFDevs__MemberBridge__FrontendOrigin=http://127.0.0.1:3000 +# for reset-email links. This is not a browser CORS allowlist. # CMS_MEMBER_COOKIE_NAME defaults to .AspNetCore.Identity.Application. # Server-only. Set these only when testing fictional local media through the same-origin proxy. @@ -15,5 +20,15 @@ CMS_INTERNAL_ORIGIN=http://127.0.0.1:5099 # MEDIA_UPSTREAM_PATH_PREFIX=/media/ # MEDIA_SOURCE_PUBLIC_ORIGIN=https://media.sgf.dev +# Adapter-node request cap: 8 MiB avatar plus multipart overhead. +BODY_SIZE_LIMIT=9M + +# Public build input. Adapter-node 6 uses paths.origin, not runtime ORIGIN. +# Export ORIGIN in the build shell; setting it only in .env/startup has no effect. +# ORIGIN=http://127.0.0.1:3000 +# HOST=127.0.0.1 +# PORT=3000 +# Leave proxy trust headers unset. Build for the external origin behind a proxy. + # Local schema refresh only. npm run api:schema:fetch reads the public OpenAPI doc from this origin. SGF_CMS_SCHEMA_ORIGIN=http://127.0.0.1:5099 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..56d246af --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: Frontend CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: npm + - run: npm ci + - run: npm run check + - run: npm run api:check + - run: npm run api:member:check + - run: npm run build + env: + ORIGIN: http://127.0.0.1:3000 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c12e1cb2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# ORIGIN is public and fixed in the SvelteKit 3 build. Never pass CMS secrets as build args. +FROM node:24-bookworm-slim AS build +WORKDIR /app +COPY package.json package-lock.json .npmrc ./ +RUN npm ci +COPY vite.config.ts tsconfig.json ./ +COPY src ./src +COPY static ./static +ARG ORIGIN +RUN test -n "$ORIGIN" && npm run build + +FROM node:24-bookworm-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000 BODY_SIZE_LIMIT=9M +COPY package.json package-lock.json .npmrc ./ +RUN npm ci --omit=dev && npm cache clean --force +COPY --from=build --chown=node:node /app/build ./build +USER node +EXPOSE 3000 +CMD ["npm", "run", "serve"] diff --git a/README.md b/README.md index 65553edb..5a58fc16 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,105 @@ # Springfield Devs website -This repository is for the public [sgf.dev](https://sgf.dev) frontend. -The Umbraco CMS and backend live in [cms.sgf.dev](https://github.com/sgfdevs/cms.sgf.dev). - -This branch starts the SvelteKit frontend rewrite with a minimal Svelte 5 app and Tailwind CSS 4 wired through Vite. It does not include the final page migration, site theme, asset migration, generated API client, or live CMS integration yet. - -## Requirements - -- Node.js 22.17 or newer. Node 24 is supported. -- npm 11 or newer is recommended because the lockfile was generated with npm 11. - -## Development - -Install dependencies: - -```sh -npm ci -``` - -Run the local dev server: +The public [sgf.dev](https://sgf.dev) frontend uses SvelteKit 3, Svelte 5 and +Tailwind CSS 4. The [Umbraco backend](https://github.com/sgfdevs/cms.sgf.dev) +runs separately. Page SSR, generated API clients, the same-origin media proxy +and member forms already call the CMS from frontend server code. Browsers do +not call the CMS member bridge directly. + +## Requirements and configuration + +Use Node.js 22.17 or newer. The container and CI use Node 24, matching the current +local runtime. npm 11 is recommended for the existing lockfile. Run `npm ci` once. +The tracked `vite.config.ts` contains the SvelteKit configuration and +`adapter-node`, including the existing runes policy and Tailwind plugin. +SvelteKit 3 does not need a separate `svelte.config.js` here. `tsconfig.json`, +`src/env.ts` and `.npmrc` are also tracked build inputs. + +Copy `.env.example` to an ignored `.env` for Vite development. Supply real keys +privately, never in Git, client code or Docker build arguments: + +- `CMS_INTERNAL_ORIGIN` is the server-only CMS origin, without a path. +- `CMS_DELIVERY_API_KEY` must match backend `Umbraco__CMS__DeliveryApi__ApiKey`. + Content-page loads require it even with native Delivery `PublicAccess=true`. + The native key can authorize preview content. Keep it private. +- `CMS_MEMBER_BRIDGE_SECRET` must match backend `SGFDevs__MemberBridge__Secret`. + Member calls fail closed without it. Match `CMS_MEMBER_COOKIE_NAME` only if + the CMS cookie name has been customized. +- Media proxy origins and path fences are private server settings. Point local + media upstreams only at fictional local media, not production storage. +- Backend `SGFDevs__MemberBridge__FrontendOrigin` must be the frontend origin, + for example `http://127.0.0.1:3000`, so reset emails link to Kit. + +Native login, session, logout and emailed password reset passed a bounded local +native-CMS journey. That builtin-member-only runtime lacked legacy SGF properties. +It cannot exercise SGF registration, profile editing or avatar uploads correctly. +Those paths still need the reviewed SGF schema. Imports remain paused; this +packaging does not install, import or activate CMS schema or content. + +## Split local development + +In the CMS shell, use an existing ordinary Development installation and privately +supply the matching bridge secret and native Delivery key. Set the reset link +origin and start the CMS without its historical launch profile: ```sh -npm run dev +cd ../cms.sgf.dev +export ASPNETCORE_ENVIRONMENT=Development DOTNET_ENVIRONMENT=Development +export SGFDevs__MemberBridge__FrontendOrigin=http://127.0.0.1:3000 +dotnet run --project SgfDevs --no-launch-profile --urls http://127.0.0.1:5099 ``` -Check Svelte and TypeScript: +This assumes that shell already has the private database, media and other CMS +settings for the owned installation. It is not a fresh-install recipe. See the +backend README for native SMTP/Mailpit settings. Do not mix those ordinary runtime +settings with sealed install-only bootstrap profiles. -```sh -npm run check -``` - -Build the production Node server: +In a separate frontend shell, with the ignored `.env` configured: ```sh -npm run build +npm run dev -- --host 127.0.0.1 --port 3000 --strictPort ``` -Preview the built app with Vite: +Vite loads `.env` during development. `npm run preview` is a Vite preview, not the +adapter-node production server. `SGF_CMS_SCHEMA_ORIGIN` is only for explicit local +schema refreshes. CI freshness checks use committed snapshots, not a live CMS. -```sh -npm run preview -``` - -Run the adapter-node production server after a build: +## Built Node server ```sh -npm run serve +npm run check +ORIGIN=http://127.0.0.1:3000 npm run build +HOST=127.0.0.1 PORT=3000 BODY_SIZE_LIMIT=9M npm run serve ``` -## Backend dependency - -The frontend will later consume APIs from `cms.sgf.dev`. That API contract and generated OpenAPI client are not part of this layer. +Before `serve`, inject private CMS/media variables into the process environment +through your private environment loader or secret store. The built server does +not load `.env` automatically. With Node 24, an ignored private dotenv file can +also be loaded explicitly with `node --env-file=.env build` instead of `npm run serve`. +No CMS secret is needed to build the source. + +This pinned adapter-node 6 uses SvelteKit `paths.origin`, not runtime `ORIGIN`. +Set the public `ORIGIN` at build time; `vite.config.ts` fixes the supplied origin +in the output. The Dockerfile refuses to build without it. A build without it +falls back to request-host inference and is not suitable for this setup. Rebuild for a different frontend origin. Setting `ORIGIN` only at +startup has no effect. For a reverse proxy, build with the external HTTPS origin, +not the internal Node listener address. Keep `PROTOCOL_HEADER`, `HOST_HEADER`, +`PORT_HEADER` and `ADDRESS_HEADER` unset. Do not trust arbitrary forwarded headers. +Restrict access to the listener behind the proxy and retain Kit's origin/CSRF +checks. `PUBLIC_SITE_ORIGIN` controls canonical metadata, not request trust. + +Set `BODY_SIZE_LIMIT=9M` for the built server to allow the existing 8 MiB avatar +limit plus multipart overhead. Application validation still limits accepted files. + +## Container and CI + +`Dockerfile` builds Node output with the existing lockfile, then installs only +production dependencies in a non-root Node 24 runtime. Supply the public origin +with `--build-arg ORIGIN=https://www.sgf.dev`. Inject private settings at container +startup, not during image creation. Inside a container, `CMS_INTERNAL_ORIGIN` +must name a CMS address reachable from that container, not its own loopback. +The image defaults to port 3000 and `BODY_SIZE_LIMIT=9M`; it does not include a CMS. + +`.dockerignore` permits only build inputs and excludes local secrets and output. +The CI workflow runs `npm ci`, check, committed public/member API freshness checks +and build. It does not deploy, use action secrets or build/push an image. diff --git a/vite.config.ts b/vite.config.ts index 5c520323..3b67e05d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,9 @@ export default defineConfig({ runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true }, - adapter: adapter() + adapter: adapter(), + // adapter-node 6 reads paths.origin, not the older runtime ORIGIN variable. + paths: process.env.ORIGIN ? { origin: process.env.ORIGIN } : undefined }) ] });