c69c028028
- scripts/seed.go: idempotent demo data per §11.11 (make seed) - forgot/reset password: SMTP-gated, one-shot TTL tokens, uniform responses against enumeration, sessions revoked on reset; login page link + pages - profile hover cards on [data-user-card] elements (§11.13) - keyboard shortcuts: g b/m/t/h navigation, / focuses search (§10) - bulk archive endpoint (§11.9) - hand-written OpenAPI 3.1 covering §6, served at /api/docs + yaml download - make backup / make restore (mongodump archive via the mongo container) - README: quick start, demo data, runbook, breaker/job operations, working Caddy + nginx reverse-proxy samples (WS block, client_max_body_size), documented later-stubs (§11.24) - smoke.sh now exercises register → logout → login → me → board → pages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
290 lines
15 KiB
YAML
290 lines
15 KiB
YAML
openapi: "3.1.0"
|
|
info:
|
|
title: Bounty Board API
|
|
version: "1.0"
|
|
description: |
|
|
Consulting work-management platform API (spec §6). All endpoints are
|
|
session-authenticated (httpOnly cookie `bb_session`); mutations also
|
|
require the CSRF double-submit header `X-CSRF-Token` mirroring the
|
|
`bb_csrf` cookie. Errors use the envelope
|
|
`{"error":{"code":"…","message":"…"}}`. Pagination: `?limit=&cursor=<ulid>`.
|
|
servers:
|
|
- url: /api/v1
|
|
tags:
|
|
- {name: auth}
|
|
- {name: admin}
|
|
- {name: consultant}
|
|
- {name: developer}
|
|
- {name: tasks}
|
|
- {name: messaging}
|
|
- {name: shared}
|
|
- {name: internal}
|
|
|
|
paths:
|
|
/auth/register:
|
|
post:
|
|
tags: [auth]
|
|
summary: Self-register a developer account
|
|
requestBody: {$ref: "#/components/requestBodies/Register"}
|
|
responses:
|
|
"201": {description: Account created and session started}
|
|
"409": {description: Email already registered}
|
|
/auth/login:
|
|
post:
|
|
tags: [auth]
|
|
summary: Local login
|
|
requestBody: {$ref: "#/components/requestBodies/Login"}
|
|
responses:
|
|
"200": {description: "Session cookies set; body: user + mustChangePassword"}
|
|
"401": {description: Invalid credentials}
|
|
"429": {description: Rate limited (10 attempts / 15 min / IP+email)}
|
|
/auth/logout:
|
|
post: {tags: [auth], summary: Revoke the current session, responses: {"204": {description: Logged out}}}
|
|
/auth/logout-all:
|
|
post: {tags: [auth], summary: Revoke every session of the current user, responses: {"200": {description: Count of revoked sessions}}}
|
|
/auth/change-password:
|
|
post:
|
|
tags: [auth]
|
|
summary: Change the local password (revokes other sessions)
|
|
responses: {"204": {description: Changed}, "401": {description: Wrong current password}}
|
|
/auth/forgot:
|
|
post:
|
|
tags: [auth]
|
|
summary: Request a password-reset email (SMTP required)
|
|
responses: {"200": {description: Uniform response}, "503": {description: SMTP not configured}}
|
|
/auth/reset:
|
|
post:
|
|
tags: [auth]
|
|
summary: Set a new password with a one-shot token
|
|
responses: {"204": {description: Password set}, "400": {description: Invalid or expired token}}
|
|
/auth/me:
|
|
get: {tags: [auth], summary: Current user, responses: {"200": {description: User profile + flags}}}
|
|
/auth/oidc/login:
|
|
get: {tags: [auth], summary: Start the OIDC code flow with PKCE (302 to the IdP), responses: {"302": {description: Redirect}}}
|
|
/auth/oidc/callback:
|
|
get: {tags: [auth], summary: OIDC redirect URI (links by verified email or creates a developer), responses: {"302": {description: Redirect home or to /login?error=…}}}
|
|
|
|
/admin/users:
|
|
get:
|
|
tags: [admin]
|
|
summary: List/search users
|
|
parameters:
|
|
- {name: q, in: query, schema: {type: string}}
|
|
- {name: role, in: query, schema: {type: string, enum: [admin, consultant, developer]}}
|
|
responses: {"200": {description: Users + nextCursor}}
|
|
/admin/users/{id}:
|
|
patch:
|
|
tags: [admin]
|
|
summary: "Update roles / disabled / name / force password reset"
|
|
responses: {"200": {description: Updated user}, "400": {description: Self-lockout guard}, "409": {description: Version conflict}}
|
|
delete: {tags: [admin], summary: Delete a user and their sessions, responses: {"204": {description: Deleted}}}
|
|
/admin/customers:
|
|
get: {tags: [admin], summary: List customers (includeArchived=true optional), responses: {"200": {description: Customers (credentials never returned)}}}
|
|
post:
|
|
tags: [admin]
|
|
summary: Create a customer with per-system credentials (encrypted at rest)
|
|
requestBody: {$ref: "#/components/requestBodies/Customer"}
|
|
responses: {"201": {description: Created}, "409": {description: Name taken}}
|
|
/admin/customers/{id}:
|
|
get: {tags: [admin], summary: Get one customer, responses: {"200": {description: Customer}}}
|
|
patch: {tags: [admin], summary: Partial update (credentials rotate when supplied), responses: {"200": {description: Updated}, "409": {description: Version conflict}}}
|
|
delete: {tags: [admin], summary: Delete (blocked while tasks exist), responses: {"204": {description: Deleted}, "409": {description: Has tasks — archive instead}}}
|
|
/admin/customers/test-connection:
|
|
post: {tags: [admin], summary: Test unsaved credentials (wizard), responses: {"200": {description: "{ok, latencyMs, error?}"}}}
|
|
/admin/customers/{id}/test-connection:
|
|
post: {tags: [admin], summary: Test the stored connection, responses: {"200": {description: "{ok, latencyMs, error?}"}}}
|
|
/admin/customers/{id}/sync-now:
|
|
post: {tags: [admin], summary: Trigger an immediate sync poll, responses: {"202": {description: Queued}}}
|
|
/admin/settings:
|
|
get: {tags: [admin], summary: Runtime settings, responses: {"200": {description: Settings}}}
|
|
patch: {tags: [admin], summary: Update runtime settings (atomizer URL override, branding, …), responses: {"200": {description: Updated settings}}}
|
|
/admin/audit-log:
|
|
get:
|
|
tags: [admin]
|
|
summary: Audit log of privileged mutations
|
|
parameters:
|
|
- {name: actorId, in: query, schema: {type: string}}
|
|
- {name: entityId, in: query, schema: {type: string}}
|
|
- {name: cursor, in: query, schema: {type: string}}
|
|
responses: {"200": {description: Entries + nextCursor}}
|
|
/admin/service-status:
|
|
get: {tags: [admin], summary: "Health/latency/breaker for atomizer + work performer, sync workers, job queue (§11.17)", responses: {"200": {description: Status payload}}}
|
|
|
|
/consultant/board:
|
|
get:
|
|
tags: [consultant]
|
|
summary: Atomization board (imported…claim_requested tasks of own customers)
|
|
parameters:
|
|
- {name: customerId, in: query, schema: {type: string}}
|
|
- {name: status, in: query, schema: {type: string}}
|
|
responses: {"200": {description: Customers + tasks}}
|
|
/consultant/reviews:
|
|
get: {tags: [consultant], summary: Tasks in review for own customers, responses: {"200": {description: Tasks}}}
|
|
/consultant/pool:
|
|
get: {tags: [consultant], summary: Global developer pool with membership flags, responses: {"200": {description: Developers}}}
|
|
post: {tags: [consultant], summary: Add a developer to my pool, responses: {"201": {description: Added}, "409": {description: Already in pool}}}
|
|
/consultant/pool/{developerId}:
|
|
delete: {tags: [consultant], summary: Remove a developer from my pool, responses: {"204": {description: Removed}}}
|
|
/consultant/metrics:
|
|
get:
|
|
tags: [consultant]
|
|
summary: "Aggregations per developer/customer + lead time + board depth (§6.2); format=csv exports the ledger"
|
|
parameters:
|
|
- {name: customerId, in: query, schema: {type: string}}
|
|
- {name: developerId, in: query, schema: {type: string}}
|
|
- {name: from, in: query, schema: {type: string, format: date}}
|
|
- {name: to, in: query, schema: {type: string, format: date}}
|
|
- {name: format, in: query, schema: {type: string, enum: [csv]}}
|
|
responses: {"200": {description: Metrics JSON or CSV}}
|
|
|
|
/board:
|
|
get:
|
|
tags: [developer]
|
|
summary: Bounty board (published tasks of consultants who pooled me)
|
|
parameters:
|
|
- {name: customerId, in: query, schema: {type: string}}
|
|
- {name: q, in: query, schema: {type: string}, description: Mongo text search}
|
|
- {name: minBounty, in: query, schema: {type: number}}
|
|
- {name: sort, in: query, schema: {type: string, enum: ["", bounty]}}
|
|
responses: {"200": {description: Tasks + customers}}
|
|
/my-tasks:
|
|
get: {tags: [developer], summary: My kanban (assigned…approved), responses: {"200": {description: Tasks}}}
|
|
/developer/metrics:
|
|
get:
|
|
tags: [developer]
|
|
summary: "Own §6.2 metrics; format=csv exports the ledger"
|
|
parameters:
|
|
- {name: from, in: query, schema: {type: string, format: date}}
|
|
- {name: to, in: query, schema: {type: string, format: date}}
|
|
- {name: format, in: query, schema: {type: string, enum: [csv]}}
|
|
responses: {"200": {description: Metrics JSON or CSV}}
|
|
/leaderboard:
|
|
get: {tags: [shared], summary: Top developers by bounty (opt-out honored), responses: {"200": {description: Leaderboard}}}
|
|
|
|
/tasks/{id}:
|
|
get: {tags: [tasks], summary: Task detail incl. timeline (role-scoped), responses: {"200": {description: Task}, "403": {description: No access}}}
|
|
patch: {tags: [consultant], summary: "Edit title/description/AC/coefficient/budget (version-checked, bounty recomputed; cascadeBudget optional)", responses: {"200": {description: Updated task}, "409": {description: Conflict or immutable}}}
|
|
/tasks/{id}/subdivide:
|
|
post: {tags: [consultant], summary: "Queue AI subdivision (§5.1); re-run needs confirmReplace", responses: {"202": {description: "{jobId}"}, "409": {description: Bad status / confirm required}}}
|
|
/tasks/{id}/extend:
|
|
post: {tags: [consultant], summary: Queue AI extension (note required), responses: {"202": {description: "{jobId}"}}}
|
|
/tasks/{id}/publish:
|
|
post: {tags: [consultant], summary: Publish an atomized task to the board, responses: {"200": {description: Task}, "409": {description: Illegal transition}}}
|
|
/tasks/publish:
|
|
post: {tags: [consultant], summary: "Bulk publish {ids:[…]}", responses: {"200": {description: "{published, failed}"}}}
|
|
/tasks/archive:
|
|
post: {tags: [consultant], summary: "Bulk archive {ids:[…]}", responses: {"200": {description: "{archived, failed}"}}}
|
|
/tasks/{id}/archive:
|
|
post: {tags: [tasks], summary: Archive (consultant/admin), responses: {"204": {description: Archived}}}
|
|
/tasks/{id}/claim:
|
|
post: {tags: [developer], summary: Request assignment (optional pitch note), responses: {"204": {description: Requested}, "409": {description: Already requested / not open}}}
|
|
/tasks/{id}/claim/withdraw:
|
|
post: {tags: [developer], summary: Withdraw my claim (back to published when none remain), responses: {"204": {description: Withdrawn}}}
|
|
/tasks/{id}/approve-claim:
|
|
post: {tags: [consultant], summary: "Approve one developer {developerId}; others are declined + notified", responses: {"204": {description: Assigned}}}
|
|
/tasks/{id}/decline-claim:
|
|
post: {tags: [consultant], summary: Decline one claim, responses: {"204": {description: Declined}}}
|
|
/tasks/{id}/assign-ai:
|
|
post: {tags: [consultant], summary: "Create a Work Performer job (§5.2) {context:{repositoryUrl,branch,instructions}}", responses: {"202": {description: "{jobId}"}, "502": {description: Performer rejected the job}}}
|
|
/tasks/{id}/unassign:
|
|
post: {tags: [consultant], summary: Return an assigned/in-progress task to the board, responses: {"204": {description: Unassigned}}}
|
|
/tasks/{id}/start:
|
|
post: {tags: [developer], summary: Start work (assigned or changes_requested), responses: {"204": {description: In progress}}}
|
|
/tasks/{id}/submit-review:
|
|
post: {tags: [developer], summary: Submit for review, responses: {"204": {description: In review}}}
|
|
/tasks/{id}/abandon:
|
|
post: {tags: [developer], summary: Abandon back to the board, responses: {"204": {description: Published}}}
|
|
/tasks/{id}/comments:
|
|
post: {tags: [tasks], summary: Add a sanitized comment (@mentions notify), responses: {"201": {description: Comment}}}
|
|
/tasks/{id}/time:
|
|
post: {tags: [developer], summary: "Log time {minutes, note}", responses: {"201": {description: Entry}}}
|
|
/tasks/{id}/review:
|
|
post: {tags: [consultant], summary: "Review {decision: approve|request_changes, note, checklist[]}; approve freezes the bounty into bountyAwards", responses: {"204": {description: Reviewed}}}
|
|
|
|
/conversations:
|
|
get: {tags: [messaging], summary: My conversations with unread counts, responses: {"200": {description: Conversations}}}
|
|
post: {tags: [messaging], summary: "Create dm (deduplicated) / group / project conversation", responses: {"200": {description: Existing dm}, "201": {description: Created}}}
|
|
/conversations/{id}/messages:
|
|
get:
|
|
tags: [messaging]
|
|
summary: Message history (newest page; cursor pages backwards)
|
|
parameters: [{name: cursor, in: query, schema: {type: string}}]
|
|
responses: {"200": {description: Messages chronological}}
|
|
post: {tags: [messaging], summary: "Send {body (sanitized rich text), attachments:[fileIds]}", responses: {"201": {description: Message}}}
|
|
/conversations/{id}/read:
|
|
post: {tags: [messaging], summary: Mark all messages read, responses: {"200": {description: "{marked}"}}}
|
|
|
|
/files:
|
|
post: {tags: [shared], summary: "Upload (multipart field `file`; scope=chat default, task for consultants)", responses: {"201": {description: "{fileId, name, mimeType, size, isImage}"}, "413": {description: Exceeds MAX_UPLOAD_MB}}}
|
|
/profile:
|
|
get: {tags: [shared], summary: Own profile, responses: {"200": {description: User}}}
|
|
patch: {tags: [shared], summary: "Partial profile update (name, bio, contact, extra, settings.theme/leaderboardOptOut); optional version → 409 on conflict", responses: {"200": {description: Updated user}}}
|
|
/profile/avatar:
|
|
post: {tags: [shared], summary: Upload avatar (must sniff as an image), responses: {"200": {description: "{fileId}"}}}
|
|
/users:
|
|
get: {tags: [shared], summary: User directory search (name/email) for messaging, responses: {"200": {description: Users}}}
|
|
/users/{id}/card:
|
|
get: {tags: [shared], summary: Public profile card (hover cards), responses: {"200": {description: Card}}}
|
|
/notifications:
|
|
get: {tags: [shared], summary: My notifications + unread count, responses: {"200": {description: Notifications}}}
|
|
/notifications/read:
|
|
post: {tags: [shared], summary: "Mark read {ids:[]} (empty = all)", responses: {"200": {description: "{marked}"}}}
|
|
/service-health:
|
|
get: {tags: [shared], summary: Atomizer + work-performer health/breaker for UI gating, responses: {"200": {description: Health}}}
|
|
|
|
/internal/work-results:
|
|
post:
|
|
tags: [internal]
|
|
summary: "Work Performer callback (§5.2): HMAC `X-Signature` over the raw body with WORK_PERFORMER_TOKEN; idempotent by jobId; artifacts ingested into GridFS"
|
|
responses:
|
|
"200": {description: Accepted or duplicate_ignored}
|
|
"401": {description: Bad signature}
|
|
|
|
components:
|
|
requestBodies:
|
|
Register:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email, name, password]
|
|
properties:
|
|
email: {type: string, format: email}
|
|
name: {type: string}
|
|
password: {type: string, minLength: 8}
|
|
Login:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email, password]
|
|
properties:
|
|
email: {type: string}
|
|
password: {type: string}
|
|
Customer:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name, type]
|
|
properties:
|
|
name: {type: string}
|
|
type: {type: string, enum: [jira, azure_devops, youtrack, demo]}
|
|
baseUrl: {type: string}
|
|
projectKey: {type: string}
|
|
pollIntervalSec: {type: integer, minimum: 10}
|
|
defaultBudget: {type: number}
|
|
consultantIds: {type: array, items: {type: string}}
|
|
credentials:
|
|
type: object
|
|
description: "jira: {email, apiToken} · azure_devops: {organization, project, pat} · youtrack: {permanentToken} · demo: {}"
|
|
schemas:
|
|
Error:
|
|
type: object
|
|
properties:
|
|
error:
|
|
type: object
|
|
properties:
|
|
code: {type: string}
|
|
message: {type: string}
|