Files

116 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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](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](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](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`
## Links & attachments (project | issue | comment)
- [ ] `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](09-notifications-realtime.md))
## Search
- [ ] `GET /api/v1/search?q=&types=issue,epic,project,comment,wiki&project_id=` — Meilisearch proxy, results filtered to caller's memberships ([10-search.md](10-search.md)); 503 when Meilisearch unconfigured
## Webhooks ([11-integrations.md](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`