Manage redirects, missing URLs, repositories and settings through the API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:51:00 +02:00
parent 4c65604f04
commit 8d0f2e3639
20 changed files with 1581 additions and 0 deletions

View File

@@ -124,6 +124,12 @@ tags:
description: The inbox — what people sent from the public site
- name: Technologies
description: What the collective uses, and its place on the tech radar
- name: Redirects
description: Old addresses, and the missing ones the frontend reported
- name: Repositories
description: The collective's git repositories and their synced numbers
- name: Settings
description: Runtime settings and the public site's maintenance mode
- name: Users
description: Local user administration
- name: Media
@@ -2201,6 +2207,449 @@ paths:
data: { $ref: "#/components/schemas/Application" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/redirects:
get:
tags: [Redirects]
summary: List redirects
parameters:
- $ref: "#/components/parameters/PerPage"
- name: is_enabled
in: query
schema: { type: boolean }
- name: locale
in: query
schema: { type: string }
- name: automatic
in: query
description: "True for the ones a slug rename left behind, false for those typed by hand."
schema: { type: boolean }
- name: search
in: query
description: "Matches either end."
schema: { type: string }
responses:
"200":
description: Paginated redirects
content:
application/json:
schema: { $ref: "#/components/schemas/RedirectCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
post:
tags: [Redirects]
summary: Create a redirect
description: |
Both ends are normalised — leading slash, no trailing one, no query
string. A `locale` of null applies in every language. Creating one marks
the matching missing URLs as resolved.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/RedirectWrite" }
responses:
"201":
description: Redirect created
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Redirect" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/redirects/{redirect}:
parameters:
- name: redirect
in: path
required: true
schema: { type: integer }
get:
tags: [Redirects]
summary: Show a redirect
responses:
"200":
description: The redirect
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Redirect" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { description: Not found }
patch:
tags: [Redirects]
summary: Update a redirect
description: Partial update.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/RedirectWrite" }
responses:
"200":
description: Redirect updated
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Redirect" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
delete:
tags: [Redirects]
summary: Delete a redirect
responses:
"204": { description: Deleted }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/missing-urls:
get:
tags: [Redirects]
summary: List missing urls
parameters:
- $ref: "#/components/parameters/PerPage"
- name: status
in: query
description: "`pending` (the default), `resolved` or `ignored`."
schema: { type: string }
- name: locale
in: query
schema: { type: string }
- name: search
in: query
description: "Matches the path."
schema: { type: string }
responses:
"200":
description: Paginated missing URLs
content:
application/json:
schema: { $ref: "#/components/schemas/MissingUrlCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/missing-urls/{missing_url}:
parameters:
- name: missing_url
in: path
required: true
schema: { type: integer }
get:
tags: [Redirects]
summary: Show a missing URL
responses:
"200":
description: The missing URL
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/MissingUrl" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { description: Not found }
patch:
tags: [Redirects]
summary: Ignore a missing URL, or stop ignoring it
description: "`status` moves between `pending` and `ignored` only: `resolved` is what giving it a redirect does."
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/MissingUrlWrite" }
responses:
"200":
description: MissingUrl updated
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/MissingUrl" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
delete:
tags: [Redirects]
summary: Delete a missing URL
responses:
"204": { description: Deleted }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/missing-urls/{missing_url}/redirect:
post:
tags: [Redirects]
summary: Give a missing URL a destination
description: |
Creates the redirect from its path, which marks it resolved. `locale`
defaults to the language it was seen in; send null for every language.
parameters:
- name: missing_url
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [to]
properties:
to: { type: string, maxLength: 500 }
status: { type: integer, enum: [301, 302], default: 301 }
locale: { type: string, nullable: true }
responses:
"201":
description: Redirect created
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Redirect" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/repositories:
get:
tags: [Repositories]
summary: List repositories
parameters:
- $ref: "#/components/parameters/PerPage"
- name: provider
in: query
description: "`gitea`, `github` or `gitlab`."
schema: { type: string }
- name: project_id
in: query
schema: { type: integer }
- name: show_publicly
in: query
schema: { type: boolean }
- name: failing
in: query
description: "Only those whose sync has failed three times or more."
schema: { type: boolean }
- name: search
in: query
description: "Matches the full name."
schema: { type: string }
responses:
"200":
description: Paginated repositories
content:
application/json:
schema: { $ref: "#/components/schemas/RepositoryCollection" }
"403": { $ref: "#/components/responses/Forbidden" }
post:
tags: [Repositories]
summary: Create a repository
description: |
Only which repository, whose project and whether to show it. Everything
else is what the provider reports on the next sync.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/RepositoryWrite" }
responses:
"201":
description: Repository created
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Repository" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/repositories/{repository}:
parameters:
- name: repository
in: path
required: true
schema: { type: integer }
get:
tags: [Repositories]
summary: Show a repository
responses:
"200":
description: The repository
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Repository" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { description: Not found }
patch:
tags: [Repositories]
summary: Update a repository
description: Partial update.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/RepositoryWrite" }
responses:
"200":
description: Repository updated
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Repository" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
delete:
tags: [Repositories]
summary: Delete a repository
responses:
"204": { description: Deleted }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/repositories/{repository}/sync:
post:
tags: [Repositories]
summary: Sync a repository
description: |
Queues a sync with the provider and answers straight away. Requires
`repositories.sync`: a sync spends a rate limit the whole application
shares.
parameters:
- name: repository
in: path
required: true
schema: { type: integer }
responses:
"202":
description: Sync queued
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Repository" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/settings:
get:
tags: [Settings]
summary: List settings
description: Every stored setting, unpaginated. Requires `settings.view`.
responses:
"200":
description: The settings
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/Setting" }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/settings/{setting}:
parameters:
- name: setting
in: path
required: true
description: "The setting's key, e.g. `site.maintenance`."
schema: { type: string }
get:
tags: [Settings]
summary: Show a setting
responses:
"200":
description: The setting
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Setting" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { description: Not found }
patch:
tags: [Settings]
summary: Update a setting
description: |
Replaces the value whole. Requires `settings.update`, or
`settings.maintenance` for `site.maintenance`. `content.version` is
written by the application and refused here.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [value]
properties:
value: { description: "Any JSON value, null included." }
responses:
"200":
description: Setting updated
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Setting" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/maintenance:
get:
tags: [Settings]
summary: Show maintenance mode
description: The public site's maintenance mode. Requires `settings.view`.
responses:
"200":
description: Maintenance state
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Maintenance" }
"403": { $ref: "#/components/responses/Forbidden" }
put:
tags: [Settings]
summary: Switch maintenance mode
description: |
Only the public site goes down; the panel and this API keep working.
`message` (language → text) and `until` are kept either way, so the
wording can be written before switching it on. A passed `until` lifts
it by itself. Requires `settings.maintenance`.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [enabled]
properties:
enabled: { type: boolean }
message: { type: object, nullable: true, additionalProperties: { type: string, maxLength: 500 } }
until: { type: string, format: date-time, nullable: true }
responses:
"200":
description: Maintenance state
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Maintenance" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/media:
post:
tags: [Media]
@@ -3659,6 +4108,130 @@ components:
links: { $ref: "#/components/schemas/PaginationLinks" }
meta: { $ref: "#/components/schemas/PaginationMeta" }
Redirect:
type: object
properties:
id: { type: integer }
from: { type: string }
to: { type: string }
status: { type: integer, enum: [301, 302] }
locale: { type: string, nullable: true, description: Null applies in every language. }
is_enabled: { type: boolean }
automatic: { type: boolean, description: Left behind by a slug rename. }
hits: { type: integer }
last_used_at: { type: string, format: date-time, nullable: true }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
RedirectWrite:
type: object
required: [from, to]
properties:
from: { type: string, maxLength: 500 }
to: { type: string, maxLength: 500, description: Different from `from`. }
status: { type: integer, enum: [301, 302], default: 301 }
locale: { type: string, nullable: true }
is_enabled: { type: boolean, default: true }
RedirectCollection:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/Redirect" }
links: { $ref: "#/components/schemas/PaginationLinks" }
meta: { $ref: "#/components/schemas/PaginationMeta" }
MissingUrl:
type: object
properties:
id: { type: integer }
path: { type: string }
locale: { type: string }
referrer: { type: string, nullable: true }
internal_referrer: { type: boolean, description: "A page of our own links here — more urgent than someone else's stale link." }
status: { type: string, enum: [pending, resolved, ignored] }
hits: { type: integer }
first_seen_at: { type: string, format: date-time, nullable: true }
last_seen_at: { type: string, format: date-time, nullable: true }
resolved_at: { type: string, format: date-time, nullable: true }
MissingUrlWrite:
type: object
required: [status]
properties:
status: { type: string, enum: [pending, ignored] }
MissingUrlCollection:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/MissingUrl" }
links: { $ref: "#/components/schemas/PaginationLinks" }
meta: { $ref: "#/components/schemas/PaginationMeta" }
Repository:
type: object
properties:
id: { type: integer }
provider: { type: string, enum: [gitea, github, gitlab] }
full_name: { type: string, description: "`owner/name`." }
project_id: { type: integer, nullable: true }
is_primary: { type: boolean }
show_publicly: { type: boolean }
html_url: { type: string, nullable: true }
description: { type: string, nullable: true }
homepage: { type: string, nullable: true }
default_branch: { type: string, nullable: true }
primary_language: { type: string, nullable: true }
license: { type: string, nullable: true }
topics: { type: array, items: { type: string } }
stars: { type: integer }
forks: { type: integer }
open_issues_count: { type: integer }
is_archived: { type: boolean }
pushed_at: { type: string, format: date-time, nullable: true }
last_synced_at: { type: string, format: date-time, nullable: true }
sync_error: { type: string, nullable: true }
sync_failures: { type: integer }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
RepositoryWrite:
type: object
required: [provider, full_name]
properties:
provider: { type: string, enum: [gitea, github, gitlab] }
full_name: { type: string, maxLength: 255, description: "`owner/name`, unique per provider." }
project_id: { type: integer, nullable: true }
is_primary: { type: boolean }
show_publicly: { type: boolean }
RepositoryCollection:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/Repository" }
links: { $ref: "#/components/schemas/PaginationLinks" }
meta: { $ref: "#/components/schemas/PaginationMeta" }
Setting:
type: object
properties:
key: { type: string }
value: { nullable: true, description: Any JSON value. }
updated_at: { type: string, format: date-time, nullable: true }
Maintenance:
type: object
properties:
enabled: { type: boolean, description: False once `until` has passed. }
message: { type: object, additionalProperties: { type: string }, description: Language → text shown to visitors. }
until: { type: string, format: date-time, nullable: true }
started_at: { type: string, format: date-time, nullable: true }
MediaFor:
type: string
enum: [pages, technologies, projects, members, clients, testimonials]