diff --git a/README.md b/README.md index d72aa5e..ab08d7a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,9 @@ npm run build NODE_ENV=production node .output/server/index.mjs ``` -The server listens on port `3000` by default; Nitro also honors `PORT`/`NITRO_PORT` and `HOST`/`NITRO_HOST`. A reverse proxy may terminate TLS and forward requests to this server. +The server listens on port `3000` by default; Nitro also honors `PORT`/`NITRO_PORT` and `HOST`/`NITRO_HOST`. A reverse proxy may terminate TLS and forward requests to this server. When using cookie authentication, configure a trusted proxy to preserve the public `Host` and overwrite `X-Forwarded-Proto`; the server uses these values to validate same-origin mutation requests. + +The health check endpoint is `GET /api/health` and returns `{ "status": "ok" }`. The upload directory must be writable and persist across deployments/restarts, and all Nuxt instances serving the same library must see the same files. Static/serverless deployments without persistent writable storage are not suitable for the current upload and media routes. Install `ffmpeg` and `ffprobe` on the host and make them available on `PATH`; audio uploads are processed after they are received. Configure an ingress or reverse-proxy request size limit appropriate for audio uploads. diff --git a/server/utils/openapi.ts b/server/utils/openapi.ts index 53be856..a184609 100644 --- a/server/utils/openapi.ts +++ b/server/utils/openapi.ts @@ -14,6 +14,7 @@ type ApiOperation = { } const operations: ApiOperation[] = [ + { method: "get", path: "/health", tag: "Health", summary: "Health check", response: "Health" }, { method: "post", path: "/auth/register", tag: "Auth", summary: "Register a user", body: "json", response: "EmptyObject", status: 201 }, { method: "post", path: "/auth/signin", tag: "Auth", summary: "Sign in", body: "json", response: "EmptyObject" }, { method: "post", path: "/auth/session", tag: "Auth", summary: "Create a session", body: "json", response: "Session", status: 201 }, @@ -119,10 +120,13 @@ function operationSpec(operation: ApiOperation) { ? { 401: jsonResponse( "Authentication required", - ref(operation.auth || operation.admin ? "UnauthorizedError" : "Error"), + ref("Error"), ), } : {}), + ...((operation.method === "post" && operation.path.startsWith("/auth/")) || ((operation.auth || operation.admin) && operation.method !== "get") + ? { 403: jsonResponse("Invalid request origin", ref("Error")) } + : {}), ...(operation.path === "/user/tracks" || (operation.path.startsWith("/admin/") && (operation.method === "patch" || operation.method === "delete")) ? { 404: jsonResponse("Resource not found", ref("NotFound")) } : {}), @@ -166,7 +170,7 @@ export const openApiDocument = { openapi: "3.1.0", info: { title: "MusicBackend", version: "v2" }, servers: [{ url: "/api", description: "Nuxt API routes" }], - tags: ["Auth", "Artist", "Album", "Genre", "Track", "All", "Search", "User", "Admin"].map((name) => ({ name })), + tags: ["Health", "Auth", "Artist", "Album", "Genre", "Track", "All", "Search", "User", "Admin"].map((name) => ({ name })), paths, components: { securitySchemes: { @@ -175,15 +179,7 @@ export const openApiDocument = { }, schemas: { Error: { type: "object", required: ["message"], properties: { message: { type: "string" } } }, - UnauthorizedError: { - type: "object", - required: ["statusCode", "statusMessage", "data"], - properties: { - statusCode: { type: "integer", enum: [401] }, - statusMessage: { type: "string", enum: ["Unauthorized"] }, - data: ref("Error"), - }, - }, + Health: { type: "object", required: ["status"], properties: { status: { type: "string", const: "ok" } } }, InvalidRequest: { oneOf: [ref("Error"), ref("H3Error")] }, H3Error: { type: "object",