feat: strengthen backup and mobile workflows
ci / gitleaks (push) Successful in 1m19s
ci / docker (push) Successful in 5m48s

This commit is contained in:
2026-09-16 21:12:52 +08:00
parent 6f38190c92
commit 6c234d7d82
64 changed files with 5059 additions and 635 deletions
+48
View File
@@ -0,0 +1,48 @@
import { csrfHeader } from '../lib/csrf'
import { ApiError, formatApiErrorDetail } from './errors'
export type JsonRequestOptions = Omit<RequestInit, 'body'> & { body?: unknown }
function apiUrl(path: string) {
return path.startsWith('/api/') ? path : `/api/v1${path.startsWith('/') ? path : `/${path}`}`
}
async function apiFetch(path: string, options: RequestInit = {}) {
const headers = new Headers(options.headers)
const csrf = csrfHeader(options.method)
if (csrf['x-csrf-token']) headers.set('x-csrf-token', csrf['x-csrf-token'])
const response = await fetch(apiUrl(path), { credentials: 'include', ...options, headers })
if (!response.ok) {
const contentType = response.headers.get('content-type') || ''
const body = contentType.includes('json') ? await response.json().catch(() => ({})) : await response.text().catch(() => '')
const detail = body && typeof body === 'object' && 'detail' in body ? body.detail : body
const code = body && typeof body === 'object' && typeof body.code === 'string'
? body.code
: detail && typeof detail === 'object' && !Array.isArray(detail) && typeof detail.code === 'string' ? detail.code : undefined
throw new ApiError(formatApiErrorDetail(detail), response.status, code, detail)
}
return response
}
export async function requestJson<T>(path: string, options: JsonRequestOptions = {}): Promise<T> {
const headers = new Headers(options.headers)
const { body: ignoredBody, ...requestOptions } = options
void ignoredBody
const body = options.body === undefined ? undefined : JSON.stringify(options.body)
if (body !== undefined) headers.set('content-type', 'application/json')
const response = await apiFetch(path, { ...requestOptions, headers, body })
return response.status === 204 ? undefined as T : await response.json() as T
}
export async function requestBlob(path: string, options: RequestInit = {}): Promise<Blob> {
return (await apiFetch(path, options)).blob()
}
export async function uploadJson<T>(path: string, file: File, options: Omit<RequestInit, 'body'> = {}): Promise<T> {
const form = new FormData()
form.append('file', file)
const response = await apiFetch(path, { ...options, method: options.method ?? 'POST', body: form })
return response.status === 204 ? undefined as T : await response.json() as T
}
export { ApiError } from './errors'