From 410eb3a5a83b731e01da1cd98856f95cd68a1885 Mon Sep 17 00:00:00 2001 From: Drew Malzahn Date: Wed, 17 Jun 2026 02:01:37 +0000 Subject: [PATCH] chore: Add deploy plumbing for the share service Add a hardened systemd unit, a Caddy reverse-proxy snippet that maps /fabula/api/* to the loopback service, and Justfile build-server/ deploy-server recipes that build a static binary and ship + restart it. Includes server/README documenting the API, config, and deploy steps. Co-Authored-By: Claude Opus 4.8 --- Justfile | 14 +++++++++++ server/Caddyfile.snippet | 21 +++++++++++++++++ server/README.md | 51 ++++++++++++++++++++++++++++++++++++++++ server/share-svc.service | 35 +++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 server/Caddyfile.snippet create mode 100644 server/README.md create mode 100644 server/share-svc.service diff --git a/Justfile b/Justfile index 130f416..4cd6b11 100644 --- a/Justfile +++ b/Justfile @@ -6,8 +6,12 @@ user := "root" host := "goldfish.malzahn.lan" www-root := "/usr/share/caddy/public_html/fabula" +# Target architecture of the deploy host (override: `just go-arch=arm64 deploy-server`) +go-arch := "amd64" + clean: rm -rf dist/* + rm -f server/share-svc serve: npm run dev @@ -18,6 +22,16 @@ build: deploy: build scp -r dist/* {{ user }}@{{ host }}:{{ www-root }}/ +# Build the share service as a static binary for the deploy host. +build-server: + cd server && CGO_ENABLED=0 GOOS=linux GOARCH={{ go-arch }} go build -o share-svc . + +# Ship the binary + unit file and (re)start the service. Requires root on host. +deploy-server: build-server + scp server/share-svc {{ user }}@{{ host }}:/usr/local/bin/share-svc + scp server/share-svc.service {{ user }}@{{ host }}:/etc/systemd/system/share-svc.service + ssh {{ user }}@{{ host }} 'systemctl daemon-reload && systemctl enable --now share-svc && systemctl restart share-svc' + format: npx prettier --write books/ npx prettier --write src/ diff --git a/server/Caddyfile.snippet b/server/Caddyfile.snippet new file mode 100644 index 0000000..31847f5 --- /dev/null +++ b/server/Caddyfile.snippet @@ -0,0 +1,21 @@ +# Caddy config for the share-link service. +# +# The frontend (served under /fabula/) calls /fabula/api/s, but the Go service +# routes plain /api/s. `uri strip_prefix /fabula` rewrites /fabula/api/s -> +# /api/s before proxying to the service on loopback. +# +# Place this INSIDE your existing site block, and make sure this /fabula/api/* +# handler comes BEFORE the static handler that serves /fabula/* — `handle` +# blocks match in source order, first match wins. + +handle /fabula/api/* { + uri strip_prefix /fabula + reverse_proxy 127.0.0.1:8090 +} + +# ... your existing static file handler for /fabula/* follows here, e.g.: +# +# handle /fabula/* { +# root * /usr/share/caddy/public_html +# file_server +# } diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..a3f4211 --- /dev/null +++ b/server/README.md @@ -0,0 +1,51 @@ +# share-svc + +Tiny Go + SQLite service that stores character-sheet JSON blobs and returns +short, content-addressed IDs, so sheets share as `...?s=` instead of a giant +inline `?c=` payload. + +## API + +| Method | Path | Body | Response | +| ------ | ------------ | ----------- | ------------------- | +| POST | `/api/s` | sheet JSON | `{"id":""}` | +| GET | `/api/s/{id}`| – | sheet JSON | + +IDs are `base64url(sha256(json))[:12]`, so identical sheets dedupe and re-sharing +is idempotent. Payloads are stored deflated in a single SQLite file. + +### Config (env) + +| Var | Default | Notes | +| ------------- | ------------- | ------------------------------------------------ | +| `ADDR` | `:8090` | Listen address. The unit binds `127.0.0.1:8090`. | +| `DB_PATH` | `shares.db` | SQLite file path. | +| `CORS_ORIGIN` | _(unset)_ | Set to allow a cross-origin dev frontend. | + +## Deploy + +From the repo root: + +```sh +just deploy-server # build (linux/amd64) + ship + restart +just go-arch=arm64 deploy-server # if the host is arm64 +``` + +That installs `/usr/local/bin/share-svc` and `share-svc.service`, then +`daemon-reload` + `enable --now` + `restart`. The SQLite DB lives in +`/var/lib/share-svc/` (created by the unit's `StateDirectory`). + +### One-time Caddy setup + +Add the `/fabula/api/*` reverse-proxy block from `Caddyfile.snippet` to your site +block (before the static `/fabula/*` handler) and reload Caddy. + +## Local dev + +`webpack serve` (port 8080) does not serve `/api/*`, so `?s=` sharing falls back +to inline `?c=` links. To exercise short links locally, run the service and point +the frontend at it: + +```sh +CORS_ORIGIN=http://localhost:8080 go run ./server +``` diff --git a/server/share-svc.service b/server/share-svc.service new file mode 100644 index 0000000..1f1731f --- /dev/null +++ b/server/share-svc.service @@ -0,0 +1,35 @@ +[Unit] +Description=Fabula Ultima share-link service +Documentation=https://git.illaoi.pro/drew/fabula-ultima-html +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/local/bin/share-svc +# Bind to loopback only; Caddy reverse-proxies public traffic to it. +Environment=ADDR=127.0.0.1:8090 +Environment=DB_PATH=/var/lib/share-svc/shares.db +Restart=on-failure +RestartSec=2 + +# Run as an ephemeral, unprivileged user. StateDirectory creates and chowns +# /var/lib/share-svc so the SQLite file persists across restarts. +DynamicUser=yes +StateDirectory=share-svc + +# Hardening +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictAddressFamilies=AF_INET AF_INET6 +RestrictNamespaces=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes + +[Install] +WantedBy=multi-user.target