99 lines
7.0 KiB
Markdown
99 lines
7.0 KiB
Markdown
# API reference
|
|
|
|
The full, searchable reference is the Integration page of the running app (`/integration`). It
|
|
renders `web/src/data/apiCatalogue.ts`, which is the single source for this page. This file is for
|
|
readers without the UI.
|
|
|
|
## Base URL
|
|
The origin that serves the app, for example `https://cms.example`. Every path starts with `/v1`,
|
|
for example `https://cms.example/v1/article/`.
|
|
|
|
## Authentication
|
|
- Public routes need nothing: `GET /v1/article/` and `GET /v1/article/{id}`. They return online
|
|
articles only.
|
|
- Everything else needs a signed-in browser session (the `jwt` cookie) or an API key:
|
|
`Authorization: Bearer rcms_...`. Create keys at `/integration/api-keys`. A key acts as its
|
|
creator, with that user's role, and shows in full once.
|
|
- A key cannot call `POST /v1/auth/verify`, `DELETE /v1/auth/register`, `/v1/api-key/*` or
|
|
`/v1/system/*`. Those answer 403 "This action needs a real session." An invalid, unknown, expired
|
|
or revoked key answers 401, even on public routes.
|
|
- Each key can make 120 requests per minute. Beyond that: 429 with a `Retry-After` header.
|
|
|
|
## Routes a key can call (24)
|
|
| Group | Routes |
|
|
| --- | --- |
|
|
| Articles | GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id} |
|
|
| Media | GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage, GET /v1/media/purge-settings (owner or admin), PATCH /v1/media/purge-settings (owner or admin), GET /v1/media/purge-preview (owner or admin), POST /v1/media/purge (owner or admin) |
|
|
| Users and invites | GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite (owner only), DELETE /v1/user/invite/{id} (owner only), POST /v1/user/invite/{id}/renew (owner only), GET /v1/auth/me |
|
|
| Settings | GET /v1/settings |
|
|
|
|
## Activity log
|
|
The activity routes are session only: a key answers 403 "This action needs a real session." and a
|
|
key never reads the audit trail. Events are append-only; there is no `POST`, `PUT`, `PATCH` or
|
|
`DELETE` under `/v1/activity`.
|
|
|
|
| Route | Who | Purpose |
|
|
| --- | --- | --- |
|
|
| `GET /v1/activity` | Any verified user | Filtered, paged events. Owner and admin see every event; any other role is scoped to its own events, and `actor` set to someone else answers 403 "Only owners and admins can do this." |
|
|
| `GET /v1/activity/summary` | Owner, admin | Counts for the range, the previous period, active people and seven-bucket series. |
|
|
| `GET /v1/activity/export` | Owner, admin | The same filters as the list without `page` or `size`, as a `text/csv; charset=utf-8` attachment (`when,actor,action,target,outcome,source,ip`). Cells starting with `=`, `+`, `-` or `@` get a leading quote. |
|
|
|
|
Query: `q`, `actor` (`user:ID`, `key:ID`, `system`), `category` (`articles`, `media`, `users`,
|
|
`api_keys`, `auth`, `system`), `outcome` (`all`, `success`, `failed`), `security` (`true`), `range`
|
|
(`today`, `7d`, `30d`, `custom`; default `7d`), `from` and `to` (dates, with `custom`), `page` (from
|
|
1) and `size` (25, 50 or 100). Any invalid value answers 400 with the body-mismatch message. Dates
|
|
are UTC. The list answers `{ items, total, page, size }`, newest first (`created_at`, then `id`).
|
|
|
|
`GET /v1/settings` also returns `activity_retention_days` (30, 90, 365 or 0 for forever, default
|
|
90), `activity_retention_max_gb` (0 for no size limit), `activity_log_size_bytes`,
|
|
`activity_last_cleanup_at` and `activity_last_cleanup_removed`. `PATCH /v1/settings` with
|
|
`{"activity_retention_days": 30}`, `{"activity_retention_max_gb": 5}` or both is session only and
|
|
owner only (403 "Only the owner can do this."); any other value answers 400. The daily cleanup
|
|
removes events past the age limit, then the oldest events until the log fits the size limit.
|
|
Whichever limit is reached first wins.
|
|
|
|
### What is recorded
|
|
Every real action is recorded by its use case, once, after the change was made (outcome `success`)
|
|
or after the use case refused it (outcome `failed`, with a plain `failure_reason`). Nothing is
|
|
recorded for a read (no `GET`, no list, no public article read, no status poll), for a request that
|
|
fails validation, or for a target that does not exist (404). A failed audit write is logged and
|
|
never changes the response or the change itself.
|
|
|
|
| Category | Actions |
|
|
| --- | --- |
|
|
| `articles` | `article.create`, `article.edit`, `article.publish`, `article.unpublish`, `article.delete` |
|
|
| `media` | `media.upload`, `media.rename`, `media.delete`, `media.merge`, `media.purge` |
|
|
| `users` | `user.register`, `user.verify`, `user.update_profile`, `invite.create`, `invite.cancel`, `invite.renew`, `invite.accept` |
|
|
| `system` | `system.media_purge` (scheduled), `system.update_checked`, `system.update_downloaded`, `system.restart_requested`, `system.activity_cleanup`, `system.activity_retention_changed` |
|
|
|
|
Target types: `article`, `image`, `user`, `invite`, `api_key` and `release` (id and name are the tag).
|
|
Only the fields `title`, `online`, `body`, `filename` and `display_name` appear in `changes`, with the
|
|
kinds `text` and `body`; a body edit carries a signed character count delta and never the text. A
|
|
change that leaves a value as it was (same body, same publish state, same file name, same display
|
|
name) records nothing. Params: `media.merge` and `media.purge` and `system.media_purge` carry
|
|
`merged` and `articles_updated`, plus `failed_groups` when a group failed (still outcome `success`);
|
|
`system.update_checked` carries `update_available`; `system.restart_requested` carries `kind` and,
|
|
for an update, `tag` (it has no target). `system.update_downloaded` is recorded when the job reaches
|
|
`ready` or `failed`, with the origin captured at request time; a refusal at the start is recorded
|
|
immediately. A failed registration or invite acceptance has an `anonymous` actor named by the
|
|
attempted username.
|
|
|
|
`failure_reason` is one fixed sentence per domain error, matched with `errors.Is`, at most 255
|
|
characters, for example "The title must be between 3 and 200 characters.", "That file name is not
|
|
valid.", "The merge request is not valid.", "A purge is already running.", "Registration is closed
|
|
on this site.", "That invitation has expired.", "The release feed could not be reached." or "The
|
|
update could not be downloaded.". Any other error becomes "Something went wrong on our side. Please
|
|
try again."; the text of the error never reaches the event, the API or the CSV. Passwords, hashes,
|
|
codes, article bodies, file bytes, storage paths and asset urls are never recorded.
|
|
|
|
Article routes accept `?format=markdown|html|json` (default `markdown`). `GET /v1/media/` accepts
|
|
`q`, `hash`, `cursor`, `filter`, `sort` and `limit`.
|
|
|
|
## Errors
|
|
Every error is a plain-text body with one sentence; the status is in the HTTP status line. The
|
|
statuses are 400, 401, 403, 404, 409, 429 and 500. Messages are listed on the Integration page.
|
|
|
|
## Notes
|
|
- Images in `body` use paths like `/static/uploadedImages/<uuid>.png`. Prefix them with the base URL.
|
|
- Browsers can call the API only from origins in the server's CORS setting.
|