docs(examples): add the CMS proof-of-concept dogfooding artifact
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 39m53s
CI / Dashboard — check (push) Successful in 10m15s

A WordPress-inspired CMS whose entire backend is PiCloud Rhai scripts
deployed with pic apply, plus a SvelteKit frontend. Built to dogfood the
project tool, CLI, and SDK; exercises docs/kv/files/users, docs+queue+cron
+pubsub triggers, the transactional-outbox notification chain, a docs
before-interceptor, set_if CAS, SSE, per-app CORS, env overlays, and a
durable Workflow (validate -> enrich || seo -> publish -> finalize) started
with workflow::start and polled with workflow::run_status, visualized live
with Svelte Flow.

FINDINGS.md / SECURITY.md capture every gap, surprise, and security issue
found (each tagged [PiCloud] vs [CMS]); the platform fixes for the
actionable ones ship in the preceding commit.

Also folds in the read-only `pic` allowlist entries added to
.claude/settings.json during the session (fewer-permission-prompts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-19 20:15:29 +02:00
parent 5682be366c
commit 813bc46640
68 changed files with 4623 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# PiCloud CMS PoC — a WordPress-inspired CMS on PiCloud
A proof-of-concept content-management system whose **entire backend is PiCloud
Rhai scripts**, deployed declaratively with `pic apply`, plus a SvelteKit
frontend. Built to **dogfood the PiCloud project tool, CLI, and SDK** — the
gaps/surprises/issues found along the way are in [FINDINGS.md](FINDINGS.md) and
the security audit is in [SECURITY.md](SECURITY.md).
> This is a **test artifact**, not a production CMS. It deliberately exercises
> as many PiCloud features as fit a blog, and records where they helped or hurt.
## What it does (WordPress-style)
| Feature | PiCloud pieces used |
|---|---|
| Admin dashboard + admin/author users | `users::` app-users + roles (`admin`/`author`/`reader`), a shared `auth` **module** as the access boundary |
| Post management + editor | `docs` collection `posts`, `kv` slug index, `kv::set_if` **CAS** view counter |
| Page management + editor | `docs` collection `pages` (one method-dispatched script on many routes) |
| Post comments + moderation | `docs` collection `comments`, a **docs `before` interceptor** that HTML-escapes + sets moderation status |
| Reader users | `users::` register/login/verify, `kv` rate-limiter |
| Reader email on new posts | `docs` trigger → **durable queue**`email::send_html`**Mailpit** |
| Post tags | `docs` tag arrays + a tag-cloud endpoint |
| Scheduled publishing | **cron** trigger flips `scheduled``published` (which re-fires the notify chain) |
| Media uploads | `files` collection (base64-in-JSON, image allowlist) |
| Live comments | `pubsub::publish_durable`**realtime SSE** (public topic) → browser `EventSource` |
| Editorial automation | **Workflow** (durable DAG: validate → enrich ∥ seo → publish → finalize) started with `workflow::start`, polled with `workflow::run_status`; live-visualized with **Svelte Flow** in the admin dashboard |
| Config / secrets / envs | `vars` (+ `picloud.dev/prod.toml` **overlays**), `secrets` (`setup-token`) |
| Deploy | `[project]` + `[app]`, declarative `pic plan/apply --dir --env`, `--prune` |
## Layout
```
picloud.toml # the whole backend: scripts, routes, triggers, interceptor, vars, secrets
picloud.dev.toml # dev env overlay picloud.prod.toml # prod env overlay
scripts/**/*.rhai # ~30 Rhai scripts (endpoints, modules, triggers, interceptor)
run-server.sh # boots picloud on :8081 against the picloud_cms DB, relaying mail to Mailpit
docker-compose.override.yml # Mailpit
frontend/ # SvelteKit SPA (public site + admin dashboard)
FINDINGS.md SECURITY.md
```
## Run it
```sh
# 0. infra: Postgres (repo compose) + Mailpit
docker compose up -d postgres # from repo root
docker run -d --name cms-mailpit -p 127.0.0.1:1025:1025 -p 127.0.0.1:8025:8025 \
-e MP_SMTP_AUTH_ACCEPT_ANY=true axllent/mailpit:latest
docker exec picloud-postgres-1 psql -U picloud -d picloud -c "CREATE DATABASE picloud_cms"
# 1. build + boot picloud (all-in-one) on :8081, relaying mail to Mailpit
cargo build -p picloud
bash examples/cms-poc/run-server.sh & # see the script for the exact env
# 2. deploy the CMS (from examples/cms-poc/)
cd examples/cms-poc
pic login --url http://localhost:8081 # admin / admin
pic apps create cms --name "PiCloud CMS" # apply can't create apps (FINDINGS F-006)
pic apps domains rm default <id> && pic apps domains add cms localhost # claim the host (F-009)
echo -n "cms-setup-secret-123" | pic secrets set --app cms setup-token
pic apply --dir . --env dev
# 3. bootstrap the first admin (no CLI to grant app-user roles — FINDINGS F-012)
curl -X POST localhost:8081/cms/auth/bootstrap -H 'Content-Type: application/json' \
-d '{"setup_token":"cms-setup-secret-123","email":"admin@cms.test","password":"adminpass1","display_name":"Site Admin"}'
# 4. frontend
cd frontend && npm install && npm run dev # http://localhost:5173
```
Then: register a reader, write+publish a post as admin, watch the email land in
Mailpit (http://localhost:8025), comment on the post, approve it, and see it
appear live.
## Backend API (all under `/cms/`, since `/api/` is reserved — FINDINGS F-007)
- Public: `GET /cms/posts`, `GET /cms/posts/:slug`, `GET /cms/posts/:id/comments`,
`POST /cms/posts/:id/comments`, `GET /cms/pages`, `GET /cms/pages/:slug`,
`GET /cms/tags`, `GET /cms/media/:id`, `POST /cms/auth/{register,login,logout}`, `GET /cms/auth/me`
- Author+: `GET/POST /cms/admin/posts`, `PUT/DELETE /cms/admin/posts/:id`, `POST /cms/admin/media`
- Admin: `/cms/admin/pages*`, `/cms/admin/comments*`, `/cms/admin/users*`
## Headlines from the audit
- 🟢 **Strengths:** the event model (docs/queue/cron/pubsub triggers), the
transactional-outbox notification chain, `set_if` CAS, the docs `before`
interceptor for write-time sanitization, `users::` + roles, SSE, and the
declarative `plan/apply/--prune/--env` loop all worked well.
- 🟠 **Rough edges:** `pic apply` can't create an app (only groups); `/api/` is a
reserved route prefix; `throw #{statusCode}` becomes a 502 that leaks script
internals; the docs filter DSL can't match array membership; user routes are
JSON-only (no form/binary bodies); no CORS; no CLI to grant app-user roles.
- 🔴 **Security root cause:** a public route holds **full app authority** and
there's **no per-row authz** — every check is app code — while uncaught errors
return a **502 that leaks the app id + script structure**. Get one `guard()`
wrong and it's a silent full bypass. See [SECURITY.md](SECURITY.md).