An instance had no way to look like itself: the top bar said "Dorfteich" whatever the operator called their instance, `instance.name` was never rendered in the running app at all, and there was no favicon anywhere — `index.html` had no `<link rel="icon">` and `public/` held only fonts and theme-init.js. Where the line is drawn, and why: - **The api never decodes an image.** Cropping, scaling and the conversion to PNG happen on a canvas in the browser; the api checks the PNG signature, reads the IHDR dimensions at their fixed offsets and enforces the caps. An image library would put a decoder in front of attacker-supplied bytes AND would have to be carried through the `--network none` offline build. Reading two big-endian integers is not decoding. - **SVG is refused**, with its own error message rather than a generic "not a PNG": it can carry script, and serving it from our own origin would be a cross-site-scripting vector. An operator who tried one should learn that it is deliberate. - **The crop is driven by number inputs, not by dragging.** A drag-only cropper excludes keyboard and switch users outright; a number input is arrow-key operable and screen-reader readable without any custom aria. The resulting pixel size is stated in text, not only drawn as a frame. - **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has already resolved `data-theme` before first paint, so the correct logo is the one painted rather than the one that appears after a flash. Without a dark variant the LIGHT logo carries both themes — the operator's own asset shown unchanged beats one they did not choose (the rule #307 extends to ponds). The settings screen warns; it never blocks. - **The favicon link is static, its resource dynamic.** index.html stays a static file and the api answers with the uploaded icon or a shipped default — that route must never 404, or the browser keeps its generic icon for good. The default is generated by a script from Node's own zlib (`gen-default-favicon.mjs`), for the same offline-build reason. - Both favicon sizes are uploaded together: one source, one crop, so the tab icon and the home-screen icon can never disagree. - Branding is served WITHOUT a session, because the login screen carries it and the browser fetches the favicon before anyone signs in. The admin screen says so — an operator may not expect their logo to be public. - The metadata is not writable through the settings endpoint: it describes bytes on disk, and hand-writing it would claim an asset that is not there. `./data/branding` follows the three-step rule #303 paid for: env default + `data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the `mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and closes the hole that made #303's variant invisible: the nightly archive skips a missing directory WORDLESSLY, so the fence now demands that every `*_DIR` the backup env declares actually travels in the archive. Verified against the real defect — removing the line fails it by name. Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start so #307 is the same event with a different scope, not a second id. Verified: api suite 103 files green (a lone `public-api` ECONNRESET under local parallel load, green in isolation — the documented local flake); branding suite 12 tests against a real directory; crop arithmetic unit tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the new section (overflow 0); and the whole flow walked in the browser: upload → crop 780×180 → stored as 512×118 → logo in the sidebar linking home with the instance name as its accessible name → topbar wordmark following `instance.name` → light logo still shown under `data-theme="dark"`.
50 lines
2.7 KiB
Docker
50 lines
2.7 KiB
Docker
# Build context is the repository root (workspace build):
|
|
# docker build -f apps/api/Dockerfile .
|
|
|
|
FROM node:22.15.1-alpine AS build
|
|
WORKDIR /repo
|
|
RUN npm install -g pnpm@11
|
|
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./
|
|
COPY packages/shared ./packages/shared
|
|
COPY packages/plugin-sdk ./packages/plugin-sdk
|
|
COPY apps/api ./apps/api
|
|
COPY deploy/fonts ./deploy/fonts
|
|
RUN pnpm install --frozen-lockfile --filter @dorfteich/api... \
|
|
&& pnpm --filter @dorfteich/shared build \
|
|
&& pnpm --filter @dorfteich/plugin-sdk build \
|
|
&& pnpm --filter @dorfteich/api build \
|
|
# Bake the self-hosted font catalog in so the PDF exporter can inline a
|
|
# pond's fonts as base64 (ADR 0016). Same download as the web image; fails
|
|
# the build if a family lacks license info.
|
|
&& FONTS_OUT=/repo/fonts node deploy/fonts/build-fonts.mjs \
|
|
# Self-contained production bundle (prod deps only, incl. the prisma CLI
|
|
# needed for migrate-on-start) at /out.
|
|
&& pnpm --filter @dorfteich/api deploy --prod --legacy /out \
|
|
&& cp -r apps/api/dist /out/dist \
|
|
&& cp -r apps/api/assets /out/assets \
|
|
&& cp -r /repo/fonts /out/fonts
|
|
|
|
FROM node:22.15.1-alpine
|
|
ARG APP_VERSION=0.0.0-dev
|
|
# Default the data dirs to the writable, node-owned locations created below, so
|
|
# the image works out of the box even where compose does not set them; compose
|
|
# still mounts named volumes here for persistence (UPLOADS_DIR/PLUGINS_DIR).
|
|
ENV NODE_ENV=production APP_VERSION=${APP_VERSION} UPLOADS_DIR=/data/uploads PLUGINS_DIR=/data/plugins CUSTOM_FONTS_DIR=/data/fonts BRANDING_DIR=/data/branding SECRETS_FILE=/data/secrets/secrets.env BACKUPS_DIR=/data/backups
|
|
WORKDIR /app
|
|
COPY --from=build --chown=node:node /out /app
|
|
# Generate the Prisma client for this image's platform.
|
|
RUN node node_modules/prisma/build/index.js generate
|
|
# A fresh named volume mounted at /data/uploads, /data/plugins, /data/fonts
|
|
# or /data/branding is created
|
|
# root-owned; pre-creating them here (Docker copies an image directory's
|
|
# ownership into a new volume on first mount) lets the non-root `node` user
|
|
# write to them. /data/backups is mounted read-only here, but pre-creating it
|
|
# node-owned keeps the shared `backups` volume writable for the backup
|
|
# sidecar even when the api container is the one that initializes it.
|
|
RUN mkdir -p /data/uploads /data/plugins /data/fonts /data/branding /data/secrets /data/backups && chown -R node:node /data/uploads /data/plugins /data/fonts /data/branding /data/secrets /data/backups
|
|
USER node
|
|
EXPOSE 3000
|
|
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
|
|
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/v1/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
CMD ["node", "dist/main.js"]
|