OpenAPI spec at /api/docs with a Scalar viewer, opt-in via FEAT_API_DOCS

This commit is contained in:
jarek committed 2026-08-17 09:50:34 +02:00
1 parent ba8670af66
commit da26f7f764
251 files changed
+32497 -66

No files matched your search

@@ -36,6 +36,14 @@ function prepareDestination(dest: any, opts: { includeEnvVars: boolean }): any {
return result;
}
/**
* GET /api/backup/destinations - List backup destinations
*
* @openapi
* summary: List all backup destinations (restic repositories); the password is stripped and cloud-credential env vars are omitted from the list view
* description: Permission denial (403, "backups:view") is produced by the shared requireBackups route guard.
* resp-200: Array of backup destination objects without secrets (no password, no envVars)
*/
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -46,6 +54,19 @@ export const GET: RequestHandler = async ({ cookies }) => {
return json(destinations.map(d => prepareDestination(d, { includeEnvVars: false })));
};
/**
* POST /api/backup/destinations - Create a backup destination
*
* @openapi
* summary: Create a restic backup destination, auto-initialize and test the repository, and register its default maintenance schedules
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* body: {name:string!, repository:string!, password:string!, envVars:{}, flags:string, hostPath:string, policies:string}
* body-example: {"name":"S3 Offsite","repository":"s3:s3.amazonaws.com/my-bucket/restic","password":"***","envVars":{"AWS_ACCESS_KEY_ID":"***","AWS_SECRET_ACCESS_KEY":"***"}}
* resp-201: The created backup destination object (includes decrypted envVars since the caller just supplied them; password is stripped)
* resp-400: Invalid input — missing name/repository/password, unsupported/SSRF-blocked repository, invalid restic flags, or an invalid cron schedule in the policies
* resp-409: A destination with this name already exists
* resp-500: Failed to create the destination (persistence error)
*/
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
@@ -37,6 +37,17 @@ function prepareDestination(dest: any, includeSecrets: boolean): any {
return result;
}
/**
* GET /api/backup/destinations/{id} - Get a single backup destination
*
* @openapi
* summary: Fetch a single backup destination; decrypted cloud-credential env vars are only included for callers who can manage backups, and the password is always stripped
* description: Permission denial (403, "backups:view") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: The backup destination object (envVars included only for "backups:manage" callers; password always stripped)
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
*/
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'view');
@@ -54,6 +65,21 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
return json(prepareDestination(destination, canManage));
};
/**
* PUT /api/backup/destinations/{id} - Update a backup destination
*
* @openapi
* summary: Update a backup destination, re-validating repository and flags when supplied and re-registering maintenance schedules when policies change
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {name:string, repository:string, password:string, envVars:{}, flags:string, hostPath:string, policies:string}
* body-example: {"name":"S3 Offsite (renamed)","policies":"{\"pruneEnabled\":true,\"pruneSchedule\":\"0 0 1 * *\"}"}
* resp-200: The updated backup destination object (password stripped, envVars echoed back to the managing caller)
* resp-400: Invalid input — invalid id, unsupported/SSRF-blocked repository, invalid restic flags, invalid policy cron, or switching to a local repository used by a remote-environment config
* resp-404: Destination not found
* resp-409: A backup using this destination is currently running, or a destination with the new name already exists
* resp-500: Update failed (persistence error)
*/
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -140,6 +166,19 @@ export const PUT: RequestHandler = async (event) => {
}
};
/**
* DELETE /api/backup/destinations/{id} - Delete a backup destination
*
* @openapi
* summary: Delete a backup destination and unregister all its maintenance and dependent backup-config schedules
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: Returns { success: true } once the destination is deleted
* resp-200-example: {"success":true}
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
* resp-409: A backup using this destination is currently running — try again once it finishes
*/
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
@@ -6,6 +6,19 @@ import { auditBackupDestination } from '$lib/server/audit';
import { getBackupDestination, updateBackupDestinationTestStatus } from '$lib/server/db';
import { initRepository } from '$lib/server/backups';
/**
* POST /api/backup/destinations/{id}/init - Initialize the restic repository
*
* @openapi
* summary: Initialize the restic repository for a destination and record the resulting test status
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: Returns { success: true, message } when the repository is initialized
* resp-200-example: {"success":true,"message":"Repository initialized"}
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
* resp-500: Repository initialization failed; returns { success: false, error }
*/
export const POST: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
@@ -9,14 +9,18 @@ import { rotateDestinationPassword } from '$lib/server/backups';
/**
* Rotate the restic repository password for a destination.
*
* Body: { currentPassword: string, newPassword: string }
*
* 200 — rotated, DB updated
* 400 — input invalid or current password wrong
* 404 — destination not found
* 409 — restic rotated but DB write failed (manual recovery needed; details
* include dbOutOfSync: true)
* 500 — restic call failed for an unrelated reason
* @openapi
* summary: Rotate the restic repository password for a destination and persist the new password
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {currentPassword:string!, newPassword:string!}
* body-example: {"currentPassword":"***","newPassword":"***"}
* resp-200: Returns { success: true } once the password is rotated and the database is updated
* resp-200-example: {"success":true}
* resp-400: Invalid input — invalid id, missing passwords, or the current password is incorrect
* resp-404: Destination not found
* resp-409: restic rotated the key but the database write failed (manual recovery needed; the response includes dbOutOfSync:true)
* resp-500: restic call failed for an unrelated reason
*/
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
@@ -7,6 +7,20 @@ import { getBackupDestination, getBackupConfigs } from '$lib/server/db';
import { runRepoTask } from '$lib/server/backups';
import { createJobResponse } from '$lib/server/sse';
/**
* POST /api/backup/destinations/{id}/task - Run a repository maintenance task
*
* @openapi
* summary: Run a restic repository maintenance task (unlock, check, prune, stats, repair-index or repair-snapshots) against a destination
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard. Streamed via createJobResponse — a task failure never surfaces as an HTTP 500; it comes back as a 200 with a failed job result (JSON-preferring callers) or a job/SSE 'result' event with success:false (streaming callers).
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {task:string!}
* body-example: {"task":"check"}
* resp-200: The task result object — { success: true, ... } on success, or { success: false, error } when the task itself fails
* resp-400: Invalid input — invalid destination id, or an unknown task name (must be one of unlock, check, prune, stats, repair-index, repair-snapshots)
* resp-403: No access to an environment whose configs use this destination (enterprise)
* resp-404: Destination not found
*/
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -8,6 +8,18 @@ import {
} from '$lib/server/db';
import { testRepository } from '$lib/server/backups';
/**
* POST /api/backup/destinations/{id}/test - Test a saved backup destination
*
* @openapi
* summary: Test connectivity to a saved backup destination's repository and update its stored test status
* description: Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* resp-200: Test result — { success: true, status: "success" } when reachable, or { success: false, status: "needs_init" | "failed", error } otherwise
* resp-200-example: {"success":true,"status":"success"}
* resp-400: Invalid id (not a number)
* resp-404: Destination not found
*/
export const POST: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
const denied = await requireBackups(auth, 'manage');
@@ -7,6 +7,18 @@ import { getBackupDestination } from '$lib/server/db';
import { verifyBackup } from '$lib/server/backups';
import { createJobResponse } from '$lib/server/sse';
/**
* POST /api/backup/destinations/{id}/verify - Verify backup integrity (streamed job)
*
* @openapi
* summary: Verify the integrity of a destination's backups by reading a data subset, streaming progress as a Server-Sent Events job
* description: Returns a text/event-stream that emits `progress` events and a final `result` event. Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard.
* path: id:integer! Backup destination id (from GET /api/backup/destinations)
* body: {dataSubset:string}
* body-example: {"dataSubset":"5%"}
* resp-200: Server-Sent Events stream of progress and a final result event ({ success, ... })
* resp-400: Invalid destination id (not a number)
*/
export const POST: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
@@ -11,6 +11,16 @@ import { withGcsCredFile } from '$lib/server/backups/restic';
* Test a backup destination configuration.
* - With `destinationId`: uses saved credentials from DB
* - Without: uses inline credentials from request body (for testing before save)
*
* @openapi
* summary: Test a backup repository — either a saved destination (by destinationId) or inline credentials supplied for a not-yet-saved destination
* description: Provide `destinationId` to test a saved destination using its stored credentials, or omit it and supply `repository`/`password`/`envVars` to test before saving. Permission denial (403, "backups:manage") is produced by the shared requireBackups route guard. destinationId from GET /api/backup/destinations.
* body: {destinationId:integer, repository:string, password:string, envVars:{}}
* body-example: {"repository":"s3:s3.amazonaws.com/my-bucket/restic","password":"***","envVars":{"AWS_ACCESS_KEY_ID":"***","AWS_SECRET_ACCESS_KEY":"***"}}
* resp-200: Test result — { success: true } when the repository is reachable, or { success: false, needsInit?, error } otherwise
* resp-200-example: {"success":true}
* resp-400: Invalid input — missing repository/password for an inline test, or an unsupported/SSRF-blocked repository
* resp-404: Destination not found (when testing a saved destinationId)
*/
export const POST: RequestHandler = async ({ request, cookies }) => {
const auth = await authorize(cookies);