diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..168c1f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +# CMS (db-less) secrets + users + build +cms/.env +cms/users.json +public/ +resources/ diff --git a/ADMIN.md b/ADMIN.md index 7c4ac92..9ece62b 100644 --- a/ADMIN.md +++ b/ADMIN.md @@ -1,68 +1,36 @@ -# Admin (Decap CMS) +# Admin (openbureau-core CMS) -A lightweight, git-based editor at `/admin`. It commits markdown straight to the -Gitea repo — no database, no server. The public site stays static; rebuilding it -after an edit is a separate step (see *Deploy* below). +The site is edited through **openbureau-core** — a small Node CMS (vendored at +`cms/core`) that builds the Hugo site and serves it together with an editor at +`/admin`. It is **DB-less**: file-based login (`auth: 'local'`) and Markdown on +disk. No Supabase, no Postgres. -## 1. Put the repo on Gitea +## What's where -The site must live in a Gitea repo (e.g. `git.kgva.ch/karim/kgva`). Set the same -`repo` and `branch` in `static/admin/config.yml`. +- `cms/kgva.config.js` — the content model (collections + fields) the editor renders. +- `cms/core/` — the engine (git subtree of `git.openbureau.ch/karim/openbureau-core`). + Update it with: + `git subtree pull --prefix=cms/core https://git.openbureau.ch/karim/openbureau-core.git main --squash` +- `cms/.env` — `JWT_SECRET` (sign-in secret) + `ADMIN_EMAILS`. Copy from `.env.example`. +- `cms/users.json` — the user store (bcrypt hashes). Git-ignored. Seed with: + `docker compose run --rm cms node /site/cms/seed-admin.mjs you@mail yourpass` -## 2. Create a Gitea OAuth app - -In Gitea: **Settings → Applications → Create OAuth2 application**. - -- Redirect URI: `https:///admin/` -- Copy the **Client ID** into `static/admin/config.yml` → `backend.app_id`. - -Decap uses PKCE, so no client secret and no auth proxy are needed. - -## 3. Edit - -Open `https:///admin/`, log in with Gitea. New project: -*Portfolio → New Project*; images upload into `static/img` and are referenced as -`/img/...`. Saving commits to the repo on the configured branch. - -## 4. Deploy (rebuild after an edit) - -Because the site is baked into the Docker image, a commit must trigger a rebuild. -Pick one: - -- **Manual:** on the host, `docker compose up -d --build`. -- **Gitea Actions** (`.gitea/workflows/deploy.yml`), sketch: - - ```yaml - on: { push: { branches: [main] } } - jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: docker compose up -d --build # on a runner with access to the host - ``` - - (Adjust to your Proxmox/LXC setup — same place OPENBUREAU deploys.) - -- **Webhook:** point a Gitea webhook at a tiny script on the host that runs the - `docker compose` line. - -## Local editing (no auth) +## Run ``` -npx decap-server # in one terminal -# uncomment `local_backend: true` in config.yml -hugo server # in another -# open http://localhost:1313/admin/ +cp cms/.env.example cms/.env # set JWT_SECRET (openssl rand -hex 32) +docker compose build +docker compose run --rm cms node /site/cms/seed-admin.mjs karim@gabrielevarano.ch # once +docker compose up -d ``` -## Self-hosting the CMS bundle (optional) +Open `http://:8080/admin/`, log in, edit. *Speichern* writes the Markdown, +*Vorschau* builds a draft preview, *Publizieren* rebuilds the public site. -To avoid the CDN (in the spirit of the colophon), download the bundle once and -point `static/admin/index.html` at it: +## Deploy -``` -curl -L https://unpkg.com/decap-cms@^3.3.3/dist/decap-cms.js \ - -o static/admin/decap-cms.js -# then in index.html: -``` +A single container (Hugo build + Node serve). See `deploy/` — +`provision-lxc.sh` stands up an LXC and does all of the above (incl. a seeded +admin whose password lands in `cms/ADMIN_PASSWORD.txt`); `Caddyfile` terminates +TLS and reverse-proxies the domain. Safe cutover: provision a NEW container on a +temporary IP, verify, point Caddy at it, then retire the old one. diff --git a/cms/.env.example b/cms/.env.example new file mode 100644 index 0000000..60c77a8 --- /dev/null +++ b/cms/.env.example @@ -0,0 +1,7 @@ +# kgva CMS — DB-less (openbureau-core, auth: local). Copy to cms/.env, fill in. +# Sign-in token secret — random: openssl rand -hex 32 +JWT_SECRET=CHANGE_ME +# Always-admin emails (comma-separated). +ADMIN_EMAILS=karim@gabrielevarano.ch +# Commit edits back to git on publish (optional). +GIT_PUBLISH=false diff --git a/cms/kgva.config.js b/cms/kgva.config.js new file mode 100644 index 0000000..6413e33 --- /dev/null +++ b/cms/kgva.config.js @@ -0,0 +1,48 @@ +// kgva site config — consumed by openbureau-core (vendored at cms/core) via +// CMS_CONFIG. DB-less: auth 'local' (file users), no plugins. Bilingual content +// (.de.md) lists as separate entries (edit each language file on its own). +export default { + site: 'kgva', + auth: 'local', + admins: ['karim@gabrielevarano.ch'], + plugins: [], + + collections: [ + { + kind: 'project', label: 'Portfolio', order: 0, + path: 'portfolio/:slug', statKey: 'portfolio', + fields: [ + { name: 'title', type: 'string', required: true }, + { name: 'slug', type: 'slug' }, + { name: 'date', type: 'date' }, + { name: 'draft', type: 'bool', default: true }, + { name: 'thumbnail', type: 'image' }, + { name: 'video', type: 'string', hint: '/media/x.mp4' }, + { name: 'studies', type: 'string', hint: '/studies/hslu/ba/semester_05' }, + { name: 'schools', type: 'list' }, + { name: 'degrees', type: 'list' }, + { name: 'semesters', type: 'list' }, + { name: 'body', type: 'markdown' }, + // NB: image galleries (`images:`) and any other frontmatter not modelled + // here are PRESERVED on save (the editor keeps unknown keys). + ], + }, + { + kind: 'page', label: 'Seiten', order: 1, fallback: true, statKey: 'pages', + fields: [ + { name: 'title', type: 'string', required: true }, + { name: 'description', type: 'string' }, + { name: 'layout', type: 'string' }, + { name: 'body', type: 'markdown' }, + ], + }, + { + kind: 'section', label: 'Index-Seiten', order: 2, index: true, statKey: 'sections', + fields: [ + { name: 'title', type: 'string' }, + { name: 'description', type: 'string' }, + { name: 'body', type: 'markdown' }, + ], + }, + ], +}; diff --git a/cms/seed-admin.mjs b/cms/seed-admin.mjs new file mode 100644 index 0000000..e0a0300 --- /dev/null +++ b/cms/seed-admin.mjs @@ -0,0 +1,9 @@ +// Seed the first admin into the local users file (DB-less auth). Run once: +// docker compose run --rm cms node /site/cms/seed-admin.mjs you@mail yourpass +import { createUser, loadUsers } from '/app/src/auth-local.js'; +const [email, password] = process.argv.slice(2); +if (!email || !password) { console.error('usage: seed-admin.mjs '); process.exit(1); } +if ((await loadUsers()).some((u) => (u.email || '').toLowerCase() === email.toLowerCase())) { + console.log('user already exists:', email); process.exit(0); +} +console.log('seeded admin', email, await createUser({ email, password, role: 'admin' })); diff --git a/deploy/provision-lxc.sh b/deploy/provision-lxc.sh index 1ba156a..320e88f 100755 --- a/deploy/provision-lxc.sh +++ b/deploy/provision-lxc.sh @@ -37,10 +37,24 @@ pct exec "$CTID" -- bash -lc ' > /etc/apt/sources.list.d/docker.list apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin rm -rf /opt/kgva && git clone '"$REPO"' /opt/kgva - cd /opt/kgva && docker compose up -d --build + cd /opt/kgva + # DB-less CMS env: random token secret (once). + [ -f cms/.env ] || sed "s#CHANGE_ME#$(openssl rand -hex 32)#" cms/.env.example > cms/.env + # the CMS runs as uid 1000 and must write content/ + build public/. + chown -R 1000:1000 /opt/kgva + docker compose build + # seed an admin on first provision; password saved to cms/ADMIN_PASSWORD.txt. + if [ ! -f cms/users.json ]; then + PW="$(openssl rand -base64 12)" + docker compose run --rm -T cms node /site/cms/seed-admin.mjs karim@gabrielevarano.ch "$PW" + printf "%s\n" "$PW" > cms/ADMIN_PASSWORD.txt + chown 1000:1000 cms/ADMIN_PASSWORD.txt cms/users.json + fi + docker compose up -d ' echo -echo "done. test the new container: curl -I http://${IP%/*}:8080" +echo "done. CMS admin at http://${IP%/*}:8080/admin/ (login: karim@… / cms/ADMIN_PASSWORD.txt)" +echo "test: curl -I http://${IP%/*}:8080" echo "then point Caddy at ${IP%/*}:8080 (see deploy/Caddyfile), reload caddy," echo "verify the domain, and only THEN remove the old container." diff --git a/docker-compose.yml b/docker-compose.yml index de797b7..bbdc63c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,30 @@ +# kgva website + CMS, powered by openbureau-core (vendored at cms/core). +# DB-LESS: local file auth (no Supabase/Postgres). The core engine builds the Hugo +# site and serves it + /admin + /api + /_preview, writing content/ on edits. +# +# Provision: cp cms/.env.example cms/.env (set JWT_SECRET), then +# docker compose build +# docker compose run --rm cms node /site/cms/seed-admin.mjs you@mail pass # once +# docker compose up -d services: - web: - build: . - image: kgva + cms: + build: + context: ./cms/core + dockerfile: api/Dockerfile + args: + HUGO_VERSION: "0.163.3" # kgva needs >= 0.163.3 + # No VITE_SUPABASE_URL → the admin uses the local-auth login (DB-less). + image: kgva-cms container_name: kgva - ports: - - "8080:80" restart: unless-stopped + environment: + CMS_CONFIG: /site/cms/kgva.config.js + SITE_DIR: /site + USERS_FILE: /site/cms/users.json + PORT: "3000" + env_file: + - cms/.env + volumes: + - .:/site + ports: + - "8080:3000"