Review, publish, preview and undo changes through the API

Revisions now call publishing only what publishes: a project's status is
whether the work is active, and a service is published by its flag.

The fake identity server answers by token, so a test can sign in a second
person.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 08:29:48 +02:00
parent 4c4a2b3299
commit 5b46436641
18 changed files with 1899 additions and 13 deletions

View File

@@ -130,6 +130,10 @@ tags:
description: The collective's git repositories and their synced numbers
- name: Settings
description: Runtime settings and the public site's maintenance mode
- name: History
description: Who changed what and when, and undoing a change
- name: Reviews
description: Review, publication and preview links
- name: Users
description: Local user administration
- name: Media
@@ -2650,6 +2654,879 @@ paths:
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/pages/{page}/revisions:
get:
tags: [History]
summary: List the history of a page
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: page
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/categories/{category}/revisions:
get:
tags: [History]
summary: List the history of a category
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: category
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/projects/{project}/revisions:
get:
tags: [History]
summary: List the history of a project
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: project
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/members/{member}/revisions:
get:
tags: [History]
summary: List the history of a member
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: member
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/services/{service}/revisions:
get:
tags: [History]
summary: List the history of a service
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: service
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/clients/{client}/revisions:
get:
tags: [History]
summary: List the history of a client
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: client
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/testimonials/{testimonial}/revisions:
get:
tags: [History]
summary: List the history of a testimonial
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: testimonial
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/talks/{talk}/revisions:
get:
tags: [History]
summary: List the history of a talk
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: talk
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/technologies/{technology}/revisions:
get:
tags: [History]
summary: List the history of a technology
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: technology
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/calls/{call}/revisions:
get:
tags: [History]
summary: List the history of a call
description: Who changed what and when, newest first. Binned records included.
parameters:
- name: call
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
- name: event
in: query
description: "`created`, `updated`, `published`, `unpublished`, `deleted`, `restored` or `erased`."
schema: { type: string }
responses:
"200":
description: Paginated revisions
content:
application/json:
schema: { $ref: "#/components/schemas/RevisionCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/revisions/{revision}/restore:
post:
tags: [History]
summary: Undo a revision
description: |
Puts back the values one revision changed, as the panel's history
does. Restoring is itself a change and lands in the history. Requires
permission to edit the record, and to publish it when the revision
touched its publication.
parameters:
- name: revision
in: path
required: true
schema: { type: integer }
responses:
"200":
description: The revision the restore recorded
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Revision" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/reviews:
get:
tags: [Reviews]
summary: What is waiting for my review
description: |
Pages, projects and services awaiting review that the caller could
publish — addressed to them, or to nobody in particular. Oldest
request first, unpaginated.
responses:
"200":
description: The queue
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/ReviewState" }
/v1/pages/{page}/reviews:
get:
tags: [Reviews]
summary: Review thread of a page
description: Requests, returns, publications and comments, newest first.
parameters:
- name: page
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: Paginated thread
content:
application/json:
schema: { $ref: "#/components/schemas/ReviewCommentCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/pages/{page}/comments:
post:
tags: [Reviews]
summary: Comment on the review of a page
parameters:
- name: page
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [body]
properties:
body: { type: string, maxLength: 2000 }
responses:
"201":
description: Comment added
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewComment" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/pages/{page}/review:
post:
tags: [Reviews]
summary: Ask for a page to be reviewed
description: |
The way to put it live without `pages.publish`: whoever can publish it
is told. `reviewer_id` addresses one of them; without it, anyone may
answer. Refused if it is already waiting or already published.
parameters:
- name: page
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
reviewer_id: { type: integer, nullable: true }
note: { type: string, nullable: true, maxLength: 2000 }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/pages/{page}/return:
post:
tags: [Reviews]
summary: Send a page back for changes
description: Tells whoever asked, with the reason. Requires `pages.publish`.
parameters:
- name: page
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [note]
properties:
note: { type: string, maxLength: 2000 }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/pages/{page}/publish:
post:
tags: [Reviews]
summary: Publish a page, now or later
description: |
Approves it: live now, or from `at` when that is in the future. Any
pending review is answered either way. Requires `pages.publish`.
parameters:
- name: page
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
at: { type: string, format: date-time, nullable: true }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/pages/{page}/unpublish:
post:
tags: [Reviews]
summary: Take a page off the site
description: Requires `pages.publish`.
parameters:
- name: page
in: path
required: true
schema: { type: integer }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/projects/{project}/reviews:
get:
tags: [Reviews]
summary: Review thread of a project
description: Requests, returns, publications and comments, newest first.
parameters:
- name: project
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: Paginated thread
content:
application/json:
schema: { $ref: "#/components/schemas/ReviewCommentCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/projects/{project}/comments:
post:
tags: [Reviews]
summary: Comment on the review of a project
parameters:
- name: project
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [body]
properties:
body: { type: string, maxLength: 2000 }
responses:
"201":
description: Comment added
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewComment" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/projects/{project}/review:
post:
tags: [Reviews]
summary: Ask for a project to be reviewed
description: |
The way to put it live without `projects.publish`: whoever can publish it
is told. `reviewer_id` addresses one of them; without it, anyone may
answer. Refused if it is already waiting or already published.
parameters:
- name: project
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
reviewer_id: { type: integer, nullable: true }
note: { type: string, nullable: true, maxLength: 2000 }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/projects/{project}/return:
post:
tags: [Reviews]
summary: Send a project back for changes
description: Tells whoever asked, with the reason. Requires `projects.publish`.
parameters:
- name: project
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [note]
properties:
note: { type: string, maxLength: 2000 }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/projects/{project}/publish:
post:
tags: [Reviews]
summary: Publish a project, now or later
description: |
Approves it: live now, or from `at` when that is in the future. Any
pending review is answered either way. Requires `projects.publish`.
parameters:
- name: project
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
at: { type: string, format: date-time, nullable: true }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/projects/{project}/unpublish:
post:
tags: [Reviews]
summary: Take a project off the site
description: Requires `projects.publish`.
parameters:
- name: project
in: path
required: true
schema: { type: integer }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/services/{service}/reviews:
get:
tags: [Reviews]
summary: Review thread of a service
description: Requests, returns, publications and comments, newest first.
parameters:
- name: service
in: path
required: true
schema: { type: integer }
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: Paginated thread
content:
application/json:
schema: { $ref: "#/components/schemas/ReviewCommentCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/services/{service}/comments:
post:
tags: [Reviews]
summary: Comment on the review of a service
parameters:
- name: service
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [body]
properties:
body: { type: string, maxLength: 2000 }
responses:
"201":
description: Comment added
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewComment" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/services/{service}/review:
post:
tags: [Reviews]
summary: Ask for a service to be reviewed
description: |
The way to put it live without `services.publish`: whoever can publish it
is told. `reviewer_id` addresses one of them; without it, anyone may
answer. Refused if it is already waiting or already published.
parameters:
- name: service
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
reviewer_id: { type: integer, nullable: true }
note: { type: string, nullable: true, maxLength: 2000 }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/services/{service}/return:
post:
tags: [Reviews]
summary: Send a service back for changes
description: Tells whoever asked, with the reason. Requires `services.publish`.
parameters:
- name: service
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [note]
properties:
note: { type: string, maxLength: 2000 }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/services/{service}/publish:
post:
tags: [Reviews]
summary: Publish a service, now or later
description: |
Approves it: live now, or from `at` when that is in the future. Any
pending review is answered either way. Requires `services.publish`.
parameters:
- name: service
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
at: { type: string, format: date-time, nullable: true }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/services/{service}/unpublish:
post:
tags: [Reviews]
summary: Take a service off the site
description: Requires `services.publish`.
parameters:
- name: service
in: path
required: true
schema: { type: integer }
responses:
"200":
description: Where the review stands
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ReviewState" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/pages/{page}/preview:
post:
tags: [Reviews]
summary: Preview links for a page
description: |
One link per language it has an address in, to see it on the real
site before it is published. Each opens this record only and expires
a few hours later.
parameters:
- name: page
in: path
required: true
schema: { type: integer }
responses:
"201":
description: Preview links
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/PreviewLinks" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/projects/{project}/preview:
post:
tags: [Reviews]
summary: Preview links for a project
description: |
One link per language it has an address in, to see it on the real
site before it is published. Each opens this record only and expires
a few hours later.
parameters:
- name: project
in: path
required: true
schema: { type: integer }
responses:
"201":
description: Preview links
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/PreviewLinks" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/services/{service}/preview:
post:
tags: [Reviews]
summary: Preview links for a service
description: |
One link per language it has an address in, to see it on the real
site before it is published. Each opens this record only and expires
a few hours later.
parameters:
- name: service
in: path
required: true
schema: { type: integer }
responses:
"201":
description: Preview links
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/PreviewLinks" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/calls/{call}/preview:
post:
tags: [Reviews]
summary: Preview links for a call
description: |
One link per language it has an address in, to see it on the real
site before it is published. Each opens this record only and expires
a few hours later.
parameters:
- name: call
in: path
required: true
schema: { type: integer }
responses:
"201":
description: Preview links
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/PreviewLinks" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/media:
get:
tags: [Media]
@@ -4323,6 +5200,83 @@ components:
until: { type: string, format: date-time, nullable: true }
started_at: { type: string, format: date-time, nullable: true }
Revision:
type: object
properties:
id: { type: integer }
event: { type: string, enum: [created, updated, published, unpublished, deleted, restored, erased] }
author:
type: object
properties:
id: { type: integer, nullable: true }
name: { type: string }
changes:
type: object
description: "Field → `{old, new}`. Translatable fields carry every language."
additionalProperties:
type: object
properties:
old: { nullable: true }
new: { nullable: true }
created_at: { type: string, format: date-time }
RevisionCollection:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/Revision" }
links: { $ref: "#/components/schemas/PaginationLinks" }
meta: { $ref: "#/components/schemas/PaginationMeta" }
ReviewComment:
type: object
properties:
id: { type: integer }
event: { type: string, enum: [requested, returned, published, commented] }
body: { type: string, nullable: true }
author:
type: object
properties:
id: { type: integer, nullable: true }
name: { type: string, nullable: true }
created_at: { type: string, format: date-time }
ReviewCommentCollection:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/ReviewComment" }
links: { $ref: "#/components/schemas/PaginationLinks" }
meta: { $ref: "#/components/schemas/PaginationMeta" }
ReviewState:
type: object
properties:
type: { type: string, enum: [pages, projects, services] }
id: { type: integer }
title: { type: string, nullable: true }
published: { type: boolean, description: On the site right now. }
published_at: { type: string, format: date-time, nullable: true, description: In the future when scheduled. }
awaiting_review: { type: boolean }
requested_at: { type: string, format: date-time, nullable: true }
requested_by: { $ref: "#/components/schemas/UserRef" }
reviewer: { $ref: "#/components/schemas/UserRef" }
UserRef:
type: object
nullable: true
properties:
id: { type: integer }
name: { type: string }
PreviewLinks:
type: object
properties:
links: { type: object, additionalProperties: { type: string, format: uri }, description: Language → link. }
expires_at: { type: string, format: date-time }
MediaFor:
type: string
enum: [pages, technologies, projects, members, clients, testimonials]