API.md: every admin route tested over HTTP and approved
123 checks across all 48 routes plus /api/docs, driven on a throwaway copy of the live database with anonymous, member, leader, admin and owner access; 0 failures. Calendar checks used 2036 probes against the real store and left it at 32 objects. API.md records, per route, the capability and what each request actually returned. tests/api_drive.py is the harness and tests/api_doc.py regenerates the document from its results, so the next approval run is a rerun, not a rewrite.
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
# Green Lane Scouts 73: Admin API
|
||||
|
||||
_Tested and approved 2026-09-05. Every route below was driven over HTTP on a throwaway copy of the
|
||||
live database with four levels of access (anonymous, member, unit leader, admin, plus the site owner
|
||||
where a route is owner-only): 123 checks, 0 failures. The calendar checks ran against the real store
|
||||
with 2036 probes and left it as found. The live registry is `GET /api/docs`; this file records what
|
||||
each route actually did when asked. Regenerate with `tests/api_drive.py` then `tests/api_doc.py`._
|
||||
|
||||
**Base:** `https://greenlanescouts73.org`. **Auth:** a browser session (cookie `s73_session`) or a
|
||||
bearer API key (`Authorization: Bearer gls73_...`), honoured on `/api/admin` only. `401` means no
|
||||
credential; `403` means a credential without the capability. Error bodies are `{"detail": ...}` or,
|
||||
from a store guardrail, `{"detail": {"error": ...}}`. Every write lands in the action log with who
|
||||
did it and what changed. Times are ISO 8601 with an offset.
|
||||
|
||||
## Identity
|
||||
|
||||
Who is calling, and the live route registry.
|
||||
|
||||
### `GET /api/admin/whoami`
|
||||
|
||||
Any credential; answers who you are, how you authenticated, and your capabilities.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| anon | 401 |
|
||||
| claude session | 200 |
|
||||
| member session | 200 |
|
||||
| admin token from LAN | 200 |
|
||||
| admin token from WAN | 403 |
|
||||
| disabled person's session is dead | 401 |
|
||||
| fresh member session after enable | 200 |
|
||||
|
||||
### `GET /api/docs`
|
||||
|
||||
Capability `api:docs`, leader and above. Generated from the router on every request.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| anon | 401 |
|
||||
| member (no api:docs) | 403 |
|
||||
| leader | 200 |
|
||||
|
||||
## Summary and leads
|
||||
|
||||
Join-form leads are read-only. A claim is who has a lead and when; it is the only thing that changes.
|
||||
|
||||
### `GET /api/admin/summary`
|
||||
|
||||
Capability `leads:read`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| anon | 401 |
|
||||
| member | 403 |
|
||||
| leader | 200 |
|
||||
|
||||
### `GET /api/admin/leads`
|
||||
|
||||
Capability `leads:read`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| leader | 200 |
|
||||
| search miss | 200 |
|
||||
| bearer key works | 200 |
|
||||
| bearer key from WAN while lan | 403 |
|
||||
| revoked key dead | 401 |
|
||||
|
||||
### `GET /api/admin/leads/{id}`
|
||||
|
||||
Capability `leads:read`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| one lead | 200 |
|
||||
| unknown lead | 404 |
|
||||
|
||||
### `POST /api/admin/leads/{id}/claim`
|
||||
|
||||
Capability `leads:read`. Take a lead so it is not sitting unclaimed. Outreach v1 is exactly
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| claim | 200 |
|
||||
| claim again (self) | 409 |
|
||||
| claim by another | 409 |
|
||||
|
||||
### `POST /api/admin/leads/{id}/release`
|
||||
|
||||
Capability `leads:read`. Let a lead go. The holder may; so may an owner (people:manage),
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| release by another admin (no people:manage) | 403 |
|
||||
| release by owner | 200 |
|
||||
| release when nobody has it | 409 |
|
||||
|
||||
### `GET /api/admin/mirrors/failed`
|
||||
|
||||
Capability `leads:read`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| failed mirrors | 200 |
|
||||
|
||||
### `POST /api/admin/mirrors/retry`
|
||||
|
||||
Capability `leads:read`. Replay leads whose copy to an external target failed. Idempotent-ish:
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| retry sheet (nothing owed) | 200 |
|
||||
| retry ntfy (not implemented) | 422 |
|
||||
|
||||
## Announcements
|
||||
|
||||
Site banners. `ends_at` is required. Revoke keeps the row.
|
||||
|
||||
### `GET /api/admin/announcements`
|
||||
|
||||
Capability `announcements:write`. Every announcement with its computed state: live, scheduled, expired,
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| list | 200 |
|
||||
|
||||
### `POST /api/admin/announcements`
|
||||
|
||||
Capability `announcements:write`. Post a notice. ends_at is required.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| no ends_at | 422 |
|
||||
| create (2036) | 201 |
|
||||
| member cannot | 403 |
|
||||
|
||||
### `DELETE /api/admin/announcements/{id}`
|
||||
|
||||
Capability `announcements:write`. Take one down early. Sets revoked_at; never deletes the row.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| revoke | 200 |
|
||||
| revoke again | 409 |
|
||||
| revoke unknown | 404 |
|
||||
|
||||
## Nearby units
|
||||
|
||||
The find-a-unit directory. Deactivate, never delete. Saving bumps `verified_at`.
|
||||
|
||||
### `GET /api/admin/nearby`
|
||||
|
||||
Capability `nearby:write`. The editing view, so deactivated rows are reachable. nearby:write
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| list | 200 |
|
||||
| bearer key out of scope | 403 |
|
||||
|
||||
### `POST /api/admin/nearby`
|
||||
|
||||
Capability `nearby:write`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| bad unit_type | 422 |
|
||||
| create | 201 |
|
||||
|
||||
### `PATCH /api/admin/nearby/{id}`
|
||||
|
||||
Capability `nearby:write`. Partial update. Saving bumps verified_at to today unless the payload
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| update | 200 |
|
||||
| unknown field rejected | 422 |
|
||||
| unknown | 404 |
|
||||
|
||||
### `DELETE /api/admin/nearby/{id}`
|
||||
|
||||
Capability `nearby:write`. Take a unit off the page. Sets active=0; never deletes the row.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| deactivate | 200 |
|
||||
| deactivate again | 409 |
|
||||
|
||||
## Our units
|
||||
|
||||
Meeting day, time and place. A unit leader edits their own unit only.
|
||||
|
||||
### `GET /api/admin/units`
|
||||
|
||||
Capability `unit:write_own`. The units the caller may edit, which is what the screen this feeds
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| pack leader sees only the pack | 200 |
|
||||
| admin sees both | 200 |
|
||||
|
||||
### `PATCH /api/admin/units/{slug}`
|
||||
|
||||
Capability `unit:write_own`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| pack leader cannot edit the troop | 403 |
|
||||
| bad time | 422 |
|
||||
| no-change save | 200 |
|
||||
| unknown | 404 |
|
||||
|
||||
## Settings
|
||||
|
||||
Typed keys only; `value: null` clears to the code default.
|
||||
|
||||
### `GET /api/admin/settings`
|
||||
|
||||
Capability `settings:write`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| leader cannot | 403 |
|
||||
| admin | 200 |
|
||||
|
||||
### `PUT /api/admin/settings/{key}`
|
||||
|
||||
Capability `settings:write`. Set one value, or clear it back to the code default with value: null.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| bad value | 422 |
|
||||
| set | 200 |
|
||||
| clear to default | 200 |
|
||||
| unknown key | 422 |
|
||||
|
||||
## API keys
|
||||
|
||||
Bearer keys, honoured on `/api/admin` only. Scopes are a subset of the owner's capabilities; a key can never mint keys. LAN-only unless `api_keys_from` is `anywhere`.
|
||||
|
||||
### `GET /api/admin/keys`
|
||||
|
||||
Capability `apikeys:own`. Your keys, newest first, with state (active / expired / revoked) and
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| admin token cannot own keys | 403 |
|
||||
| own list | 200 |
|
||||
|
||||
### `POST /api/admin/keys`
|
||||
|
||||
Capability `apikeys:own`. Mint a key. Body: label, scopes (list), expires_days (optional, 1-365).
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| unscopable scope | 422 |
|
||||
| mint | 201 |
|
||||
| a key cannot mint keys | 403 |
|
||||
|
||||
### `DELETE /api/admin/keys/{id}`
|
||||
|
||||
Capability `apikeys:own`. Revoke one of your keys. The row stays. A key that is not yours is
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| revoke | 200 |
|
||||
| revoke again | 409 |
|
||||
| foreign/unknown key | 404 |
|
||||
|
||||
## History
|
||||
|
||||
The action log, newest first. Admin and above.
|
||||
|
||||
### `GET /api/admin/history`
|
||||
|
||||
Capability `history:read`. Newest first. `kind` is a prefix filter (calendar., nearby., login.).
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| leader cannot | 403 |
|
||||
| admin, filtered | 200 |
|
||||
|
||||
## People
|
||||
|
||||
Invites and reset links are returned once and handed over by the admin; there is no outbound mail. Grants are limited to what the caller holds. Owner-only: roles, disable, enable.
|
||||
|
||||
### `GET /api/admin/people`
|
||||
|
||||
Capability `people:invite_leader`. Everyone with an account, their roles and memberships, active
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| leader cannot | 403 |
|
||||
| admin | 200 |
|
||||
|
||||
### `POST /api/admin/people/invite`
|
||||
|
||||
Capability `people:invite_leader`. Mint an invite. Body: email, global_role (optional), units
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| bad email | 422 |
|
||||
| admin cannot grant owner | 403 |
|
||||
| invite | 201 |
|
||||
|
||||
### `DELETE /api/admin/people/invite/{id}`
|
||||
|
||||
Capability `people:invite_leader`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| revoke invite | 200 |
|
||||
| revoke again | 404 |
|
||||
|
||||
### `PUT /api/admin/people/{id}/roles`
|
||||
|
||||
Capability `people:manage`. Replace a person's global role and unit memberships. Body:
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| admin cannot change roles (owner only) | 403 |
|
||||
| owner sets roles | 200 |
|
||||
|
||||
### `POST /api/admin/people/{id}/disable`
|
||||
|
||||
Capability `people:manage`. Disable a person now: sessions end, keys stop, documents close. The
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| admin cannot disable | 403 |
|
||||
| owner disables | 200 |
|
||||
|
||||
### `POST /api/admin/people/{id}/enable`
|
||||
|
||||
Capability `people:manage`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| owner enables | 200 |
|
||||
|
||||
### `POST /api/admin/people/{id}/reset`
|
||||
|
||||
Capability `people:invite_admin`. Mint a one-time password-reset link (24 h), returned ONCE. Their
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| admin mints a reset link | 201 |
|
||||
| owner may reset an admin | 201 |
|
||||
| unknown person | 404 |
|
||||
|
||||
## Roster and family
|
||||
|
||||
Families, scouts, and a per-year checklist recording that a thing was collected, never the thing. Nothing medical is stored. `/family` is the signed-in parent's own households.
|
||||
|
||||
### `GET /api/admin/roster`
|
||||
|
||||
Capability `roster:write`. Households with their scouts and this year's checklist. `unit` is a
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| member cannot | 403 |
|
||||
| roster by unit | 200 |
|
||||
|
||||
### `POST /api/admin/roster/households`
|
||||
|
||||
Capability `roster:write`. A family. Body: parent_name (required), email, phone, second_parent,
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| no parent | 422 |
|
||||
| create family | 201 |
|
||||
| import lead | 201 |
|
||||
| import twice | 409 |
|
||||
|
||||
### `GET /api/admin/roster/households/{id}`
|
||||
|
||||
Capability `roster:write`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| one family | 200 |
|
||||
|
||||
### `PATCH /api/admin/roster/households/{id}`
|
||||
|
||||
Capability `roster:write`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| deactivate family | 200 |
|
||||
|
||||
### `POST /api/admin/roster/households/{id}/scouts`
|
||||
|
||||
Capability `roster:write`. Body: first_name (required), last_name, unit_id (required), den, bsa_member_id.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| add scout | 201 |
|
||||
| bad bsa id | 422 |
|
||||
|
||||
### `PUT /api/admin/roster/households/{id}/people`
|
||||
|
||||
Capability `people:invite_leader`. Which accounts belong to this family: body {person_ids: [...]}. This is
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| leader cannot link accounts | 403 |
|
||||
| unknown person id | 422 |
|
||||
| admin links the member | 200 |
|
||||
|
||||
### `PATCH /api/admin/roster/scouts/{id}`
|
||||
|
||||
Capability `roster:write`.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| edit scout | 200 |
|
||||
|
||||
### `PUT /api/admin/roster/scouts/{id}/checks/{item}`
|
||||
|
||||
Capability `roster:write`. Mark an item collected for this year: body {done: true|false, note}.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| mark dues | 200 |
|
||||
| bad item | 422 |
|
||||
|
||||
### `GET /api/admin/family`
|
||||
|
||||
Capability `account:self`. The signed-in person's own families: scouts, dens, and this year's
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| member sees own family | 200 |
|
||||
| unlinked account sees none | 200 |
|
||||
| inactive family drops off | 200 |
|
||||
| anon | 401 |
|
||||
|
||||
## Calendar
|
||||
|
||||
Writes go to Radicale. The site owns its two UID namespaces and refuses any other before a network call. DELETE really deletes.
|
||||
|
||||
### `GET /api/admin/calendar`
|
||||
|
||||
Capability `calendar:write`. Every event the public calendar shows, newest first is NOT the order:
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| member cannot | 403 |
|
||||
| list | 200 |
|
||||
|
||||
### `POST /api/admin/calendar`
|
||||
|
||||
Capability `calendar:write`. Add an event. Body: title, date (YYYY-MM-DD), unit (pack|troop|both),
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| no title | 422 |
|
||||
| create (2036) | 201 |
|
||||
|
||||
### `PUT /api/admin/calendar/{uid}`
|
||||
|
||||
Capability `calendar:write`. Replace one site-owned event in full (same body as POST). A UID the
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| replace | 200 |
|
||||
| foreign uid | 403 |
|
||||
|
||||
### `DELETE /api/admin/calendar/{uid}`
|
||||
|
||||
Capability `calendar:write`. Remove one site-owned event from the store. Unlike everything else on
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| delete | 200 |
|
||||
| delete again | 404 |
|
||||
|
||||
## Facebook posts
|
||||
|
||||
The publisher reports; leaders read, cancel and edit. The image hash is verified on arrival and the image route is gated like the list. Cancel and reschedule go through the publisher's signed per-post link.
|
||||
|
||||
### `GET /api/admin/fbposts`
|
||||
|
||||
Capability `fbposts:read`. Every post the publisher has reported, newest scheduled first, with
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| member cannot | 403 |
|
||||
| listed as scheduled | 200 |
|
||||
| published dropped from open list | 200 |
|
||||
|
||||
### `POST /api/admin/fbposts/ingest`
|
||||
|
||||
Capability `fbposts:ingest`. scout-publisher reports a post. Body: id (<unit>/<queue-stem>), unit,
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| leader cannot ingest | 403 |
|
||||
| hash mismatch refused | 422 |
|
||||
| ingest with image | 200 |
|
||||
| past time reads as published | 200 |
|
||||
|
||||
### `GET /api/admin/fbposts/{id}/image`
|
||||
|
||||
Capability `fbposts:read`. The stored image, behind the same gate as the list.
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| image anon | 401 |
|
||||
| image gated | 200 |
|
||||
|
||||
### `POST /api/admin/fbposts/{id}/cancel`
|
||||
|
||||
Capability `fbposts:read`. Cancel a scheduled post through the publisher's own signed link, then
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| cancel: publisher refuses a bad signature | 502 |
|
||||
| cancel after time passed | 409 |
|
||||
|
||||
### `POST /api/admin/fbposts/{id}/reschedule`
|
||||
|
||||
Capability `fbposts:read`. Edit and repost a scheduled post: body {message, scheduled_for}. The
|
||||
|
||||
| Tried | HTTP |
|
||||
|---|---|
|
||||
| reschedule: time too soon | 422 |
|
||||
| reschedule: publisher refuses a bad signature | 502 |
|
||||
|
||||
Reference in New Issue
Block a user