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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user