Files
plainpages/plugins/scheduling/README.md
lilleman f189f88942 §7 reference plugin (todo §7); plugins/scheduling is the worked example of the plugin contract — a list page fetching upstream data, a CSRF-guarded form forwarding writes upstream, permission-gated nav. shifts.ts: an injectable-fetch upstream REST client (stateless stand-in for the customer backend) + thin handler factories (list filters by ?q + degrades to a recoverable page on upstream-down; create CSRF-guards via ctx.verifyCsrf, validates, forwards, PRG, 502 on upstream 4xx). plugin.ts: apiVersion literal, namespaced scheduling:read/write perms, nav gated so the whole Scheduling header vanishes for non-holders. Views compose the core building blocks around the native app shell, incl. the plugin's own partials/shift-form. New host capability so a plugin page is native + secure (src/chrome.ts buildPluginChrome): ctx.chrome = brand/global-nav/user/theme/csrf for partials/shell (global menu = Dashboard + every plugin nav fragment + gated admin section, role-filtered + current-marked); ctx.verifyCsrf = the host's bound double-submit verifier (secret stays in the host). Both added to RequestContext (defaulted in buildContext), built per plugin route in app.ts (CSRF cookie set when fresh). Dashboard merges plugin nav fragments too (gated => invisible to anonymous, visual E2E byte-identical). Out of the box: bootstrap grants the demo admin scheduling:read/write (seedAdmin generalized to a roles list, env ADMIN_ROLES); dev compose runs a tiny stdlib mock upstream (examples/shifts-upstream, SCHEDULING_UPSTREAM). plugins/ added to tsconfig + the npm test glob. Tests-first across shifts/chrome/app/dashboard/bootstrap. README Building-a-plugin + Layout and docs/plugin-contract.md (ctx.chrome/verifyCsrf, upstream pattern) updated. typecheck + 296 units + the Ory-free visual E2E green (plugin discovered at boot, routes/nav gated, dashboard unchanged); live full-stack boot-verified (stack up with plugin + mock upstream serving the seeded shifts, bootstrap grants in real Keto all allowed:true) then torn down. apiVersion stays 1.0.0 (contract still assembled in §7). Authenticated browser happy-path deferred to §8 full E2E (line 114).
2026-06-19 14:48:27 +02:00

1.7 KiB

Scheduling — the reference plugin

A worked example of the plugin contract. Copy this folder, rename it (the folder name becomes the plugin id and mount path), and point it at your own backend.

What it demonstrates:

  • A list page that fetches upstream dataGET /scheduling/shifts calls the upstream REST service and renders the rows with the core building blocks (shifts.ejs → app shell, filter-bar, data-table). Search round-trips the URL; zero-JS.
  • A form that forwards a write upstreamGET /scheduling/shifts/new renders the form, POST /scheduling/shifts CSRF-verifies it (ctx.verifyCsrf) and forwards the create upstream, then POST-redirect-GET. The form body lives in the plugin's own views/partials/shift-form.ejs, reusing the core field partial.
  • Permission-gated nav — the "Shifts" nav leaf and routes are gated on scheduling:read / scheduling:write; the whole "Scheduling" section is invisible to anyone without the grant.

The plugin holds no state — data lives upstream (README → Stateless). Handlers are thin and fetch is injectable, so they unit-test as pure functions (shifts.test.ts).

Upstream

Set SCHEDULING_UPSTREAM to your backend's base URL (it must expose GET /shifts and POST /shifts). The dev compose points it at a tiny in-memory mock (examples/shifts-upstream/) so docker compose up shows the plugin working out of the box.

Granting access

A user sees Scheduling once they hold the scheduling:read role in Keto (and scheduling:write to create). The one-command bootstrap grants both to the demo admin, so the seeded admin@plainpages.local can use it immediately.