Files
solopm-server/plans/04-api.md
T

7.9 KiB
Raw Blame History

04 — API Surface

Conventions

  • Base path /api/v1; JSON in/out; UTF-8
  • Auth: session cookie (scs) or Authorization: Bearer solopm_… PAT — same middleware resolves both (01-architecture.md)
  • Pagination: ?page=1&per_page=50 (max 100); list responses {items: [...], page, per_page, total}
  • Filtering: documented per endpoint below; multiple values comma-separated (status=backlog,planned)
  • Sorting: ?sort=field / ?sort=-field (descending)
  • Errors: {"error": {"code", "message", "fields?"}}; non-member project access → 404
  • Issues are addressed by project_issue_id ({num}) within their project, never by global id, in all /projects/{id}/issues/{num} routes
  • Mutating endpoints emit domain events per 03-domain-events.md

Auth & account

  • POST /api/v1/auth/register — email, username, name, password; first user becomes admin
  • POST /api/v1/auth/login — email + password → session cookie
  • POST /api/v1/auth/logout — destroy session
  • GET /api/v1/auth/oauth/{provider} — redirect to GitHub/Gitea authorize (provider ∈ github|gitea)
  • GET /api/v1/auth/oauth/{provider}/callback — code exchange → upsert oauth_accounts → session cookie → redirect to SPA
  • GET /api/v1/me — current user + memberships summary + unread notification count
  • PATCH /api/v1/me — name, avatar_url, email_mode
  • PUT /api/v1/me/password — requires current password
  • GET /api/v1/me/tokens — list PATs (prefix, name, last_used_at; never the token)
  • POST /api/v1/me/tokens — create PAT; plaintext token returned exactly once
  • DELETE /api/v1/me/tokens/{id} — revoke
  • GET /api/v1/users — pickable users for member/assignee selectors (excludes system, zombie, deleted)

Admin (RequireAdmin)

  • GET /api/v1/admin/users — all users incl. soft-deleted
  • PATCH /api/v1/admin/users/{id} — toggle is_admin
  • DELETE /api/v1/admin/users/{id} — soft-delete user (blocked with conflict if sole owner of any project — transfer first)
  • POST /api/v1/admin/cull?dry_run=true|false — run retention job; dry-run returns would-delete report (12-retention.md)
  • POST /api/v1/admin/search/reindex — full Meilisearch rebuild
  • GET /api/v1/admin/jobs?status=dead — inspect job queue

Projects

  • GET /api/v1/projects — my projects, favorites first then recent activity; filters: status, q
  • POST /api/v1/projects — creates project + owner membership + seeded labels
  • GET /api/v1/projects/{id} — detail incl. members, labels, dependencies, my-favorite flag
  • PATCH /api/v1/projects/{id} — fields incl. status (sets/clears completed_at)
  • DELETE /api/v1/projects/{id} — soft delete (owner only)
  • PUT /api/v1/projects/{id}/owner — transfer ownership to another member (owner only)
  • PUT /api/v1/projects/{id}/favorite / DELETE …/favorite
  • GET /api/v1/projects/{id}/members / POST (owner only; body: user_id, role) / PATCH …/members/{userId} (role) / DELETE …/members/{userId} (owner only; owner cannot remove self without transfer)
  • GET /api/v1/projects/{id}/dependencies / POST (409 on cycle) / DELETE …/dependencies/{depId}
  • GET /api/v1/projects/{id}/labels / POST / PATCH …/labels/{labelId} / DELETE …/labels/{labelId} (soft)
  • GET /api/v1/projects/{id}/activity — paged audit feed for the project
  • GET /api/v1/dashboard/activity — cross-project feed over my projects
  • GET /api/v1/projects/{id}/export?format=json|csv — streaming download (11-integrations.md)

Epics

  • GET /api/v1/projects/{id}/epics — list with progress (issue counts by status + story-point totals); filter status
  • POST /api/v1/projects/{id}/epics
  • GET /api/v1/projects/{id}/epics/{epicId} — detail incl. ordered issues
  • PATCH /api/v1/projects/{id}/epics/{epicId} / DELETE (soft; issues keep existing but epic_id set NULL at cull time, not delete time)
  • PUT /api/v1/projects/{id}/epics/{epicId}/issues/{num} — attach/move issue into epic (body: after_rank for position)
  • DELETE /api/v1/projects/{id}/epics/{epicId}/issues/{num} — detach

Issues

  • GET /api/v1/projects/{id}/issues — filters: status, assignee, label, priority, epic, parent, q (title substring); sort: any column; ?view=board returns grouped-by-status arrays ordered by board_rank
  • POST /api/v1/projects/{id}/issues — mints project_issue_id; optional epic_id, parent, assignee, labels
  • GET /api/v1/projects/{id}/issues/{num} — full detail (labels, watchers, blockers, sub-issues, epic, attachment/link lists)
  • PATCH /api/v1/projects/{id}/issues/{num} — any field; status→done sets completed_at; board moves send {status, after_rank}
  • DELETE /api/v1/projects/{id}/issues/{num} — soft delete
  • GET /api/v1/projects/{id}/issues/{num}/activity — audit entries comments, ascending
  • GET /api/v1/projects/{id}/issues/{num}/blockers / POST (409 on cycle) / DELETE …/blockers/{blockerId}
  • PUT /api/v1/projects/{id}/issues/{num}/labels/{labelId} / DELETE
  • PUT /api/v1/projects/{id}/issues/{num}/watch — watch (clears muted) / DELETE — unwatch (sets muted)
  • GET /api/v1/projects/{id}/issues/{num}/comments / POST
  • PATCH /api/v1/comments/{commentId} (author only) / DELETE (soft; author or project owner)
  • PUT /api/v1/comments/{commentId}/resolve / DELETE …/resolve
  • POST /api/v1/projects/{id}/links · POST /api/v1/projects/{id}/issues/{num}/links · POST /api/v1/comments/{commentId}/links
  • DELETE /api/v1/links/{linkId} (membership checked via owning resource)
  • POST /api/v1/projects/{id}/attachments · POST /api/v1/projects/{id}/issues/{num}/attachments · POST /api/v1/comments/{commentId}/attachments — multipart, size ≤ MAX_UPLOAD_MB
  • DELETE /api/v1/attachments/{attachmentId} (soft; file removed at cull)
  • GET /files/{path} — auth-checked attachment serving (verify requester is member of the owning project; no directory listing; Content-Disposition from original_filename)

Wiki

  • GET /api/v1/projects/{id}/wiki — page list (title, slug, updated_by, updated_at)
  • POST /api/v1/projects/{id}/wiki — create (slug from title; 409 on duplicate)
  • GET /api/v1/projects/{id}/wiki/{slug} / PATCH / DELETE (soft)

Notifications & real-time

  • GET /api/v1/notifications?unread=true — paged inbox
  • GET /api/v1/notifications/unread_count
  • PUT /api/v1/notifications/{id}/read / PUT /api/v1/notifications/read_all
  • GET /api/v1/stream — SSE; frames notification, data, ping (09-notifications-realtime.md)
  • GET /api/v1/search?q=&types=issue,epic,project,comment,wiki&project_id= — Meilisearch proxy, results filtered to caller's memberships (10-search.md); 503 when Meilisearch unconfigured

Webhooks (11-integrations.md)

  • POST /webhooks/incoming/{provider} — unauthenticated endpoint, HMAC-SHA256 verified, writes receipt, processes (outside /api/v1)
  • GET /api/v1/projects/{id}/webhook_receipts — paged receipts
  • POST /api/v1/projects/{id}/webhook_receipts/{rid}/replay
  • GET /api/v1/projects/{id}/webhooks / POST / PATCH …/webhooks/{whId} / DELETE (all owner only)
  • POST /api/v1/projects/{id}/webhooks/{whId}/test — send a signed test payload now
  • GET /api/v1/projects/{id}/webhooks/{whId}/deliveries — delivery log

Ops

  • GET /healthz — 200 with {db: ok, meilisearch: ok|absent, smtp: configured|absent}
  • All non-/api, non-/webhooks, non-/files GETs → embedded SPA index.html