Public REST API v1: personal access tokens, instance switch + per-pond opt-in #104
Labels
No Label
area:auth
area:docs
area:export
area:ops
area:storage
area:supply-chain
auth
backend
blocked
collab
deployment
docs
effort:L
effort:M
effort:S
frontend
plugins
qa
vs-nfd
vs-nfd:blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stwaidele/dorfteich#104
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Dorfteich is only reachable through the browser session today (
dt_sessioncookie,auth.guard.ts). Scripts, integrations and the upcoming MCP endpoint (follow-up issue) need a stable, token-authenticated public API.Scope
read|write, optional restriction to selected ponds). The token value is shown once and stored hashed (auth-tokens pattern); tokens are revocable and carry a last-used timestamp. A token acts AS its user — the entire permission system (grants, label scopes, 404-vs-403 per #60) applies unchanged. Auth viaAuthorization: Bearer dt_pat_...: a new guard branch beside the session cookie, CSRF-exempt (no cookie involved), rate-limited per token (existingRateLimitService), audit-logged (api.token_created/api.token_revoked, plus request auditing for writes).api.enabled(default off, admin settings UI) + per-pond opt-inapiEnabledin the pond settings (default off;pondSettingsSchemapattern, toggle in the pond settings page). Disabled instance or non-opted-in pond → the API answers 404 for that content (consistent with #60)./api/public/v1/…, JSON, versioned; OpenAPI document served at/api/public/v1/openapi.json):GET /me— the token's user + scopes (client smoke test)GET /ponds— API-enabled ponds visible to the token user;GET /ponds/{slug}— metadataGET /ponds/{slug}/pages— page list (slug, title, labels, updatedAt)GET /ponds/{slug}/pages/{pageSlug}— page as Markdown + rendered HTML + metadataPOST /ponds/{slug}/pages— create a page from title + Markdown (sharedmarkdownToDocpipeline)PATCH /ponds/{slug}/pages/{pageSlug}— update title and/or replace content from Markdown. Content replacement must go through the collab-owned document path (same pg-NOTIFY mechanism as the version restore) so open editors converge and no second document lineage appears.DELETE /ponds/{slug}/pages/{pageSlug}— move to trashGET /search?q=&pond=&label=— permission-filtered search (reuses the #49 provider incl. the substring fallback)GET /ponds/{slug}/export/markdown— ZIP export (reusesExportService.streamPondMarkdownZip)GET/POST /ponds/{slug}/labels,PATCH/DELETE /labels/{id}(rename, recolor, move, delete),PUT/DELETE /ponds/{slug}/pages/{pageSlug}/labels/{labelId}(assign/unassign)GET/POST /ponds/{slug}/pages/{pageSlug}/comments+ resolve/unresolveAcceptance criteria
scope_requiredTechnical notes
api_tokenstable (hash, scopes, pond restriction, expiry, lastUsedAt) + guard branch inapps/api/src/auth/auth.guard.ts; token hashing likeAuthTokensService(SHA-256).apps/api/src/public-api/) and call the existing services (pages/labels/comments/search/export) — no logic duplication.RateLimitService(per-token keys) andAuditService.Dependencies
None (prerequisite for the MCP endpoint issue).
Size: ~3–4 days
Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add
deanden), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.Implemented in
0c85293(+52975badeflaking a pre-existing race in the collab restore-listener test that the CI run surfaced). CI fully green, deployed to Test + Int.What shipped
api_tokenstable (SHA-256 hash, scope read|write, optional pond restriction, expiry, revocation, throttled last-used), migration20260712020000_api_tokens. Secretsdt_pat_…, shown exactly once. Lifecycle endpoints under/users/me/api-tokensare session-only — a leaked token can never mint more tokens. Settings-UI section (create/reveal-once/revoke/status) de+en. Audit:api.token_created/api.token_revoked.api.enabled(admin switch) + pond settingapiEnabled(pond-settings toggle), both default off; 404 semantics per #60 on both levels. (PondsService.updatesettings-merge covers the new key — the #92 lesson.)/api/public/v1(excluded from the SPA prefix): me, ponds, pages (list, read as Markdown+HTML, create from Markdown via the shared pipeline, PATCH title/content, DELETE→trash), search (permission-filtered, narrowed to exposed ponds,**…**highlights), markdown-ZIP export, labels (tree/CRUD/assign), comments (threads/create/resolve/reopen). Hand-maintained OpenAPI 3.1 at/openapi.json, pinned to the controller by a two-way route-coverage test.PublicApiGuard(instance switch → bearer PAT → per-token rate limit 429+Retry-After → scope 403scope_required→ pond opt-in + token restriction), then the unchanged sharedPermissionGuard(PageParamSourcegainedpondSlugParam). No cookies → no CSRF surface (pinned by a hostile-Origin test). Every write audit-logged asapi.writewith the token attributed.Deviations from the issue text (deliberate): label routes are nested under the pond (
PATCH/DELETE /ponds/{slug}/labels/{id}instead of/labels/{id}) so the pond opt-in gate stays uniform; one PATCH covers rename+recolour+move.Verification: 12-test e2e pack (lifecycle, switches, permission matrix reader/editor/outsider × scopes × opt-in, restriction, roundtrip incl. restore-NOTIFY assertion, comments policy, search narrowing, rate limit); full api suite green; UI smoke against the built SPA 10/10; docs
docs/self-hosting/public-api.md.Lesson recorded: never raise the instance-default quota in shared-DB test suites — the pack now uses a per-user quota override (the parallel quota suites caught the earlier version).