441 lines
9.2 KiB
TypeScript
441 lines
9.2 KiB
TypeScript
import { OpenAPIRegistry, OpenApiGeneratorV3, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
|
import { z } from 'zod';
|
|
|
|
extendZodWithOpenApi(z);
|
|
|
|
const registry = new OpenAPIRegistry();
|
|
|
|
const ErrorResponseSchema = registry.register(
|
|
'ErrorResponse',
|
|
z.object({
|
|
message: z.string(),
|
|
errors: z.unknown().optional(),
|
|
}),
|
|
);
|
|
|
|
const VideoRecordSchema = registry.register(
|
|
'VideoRecord',
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
objectKey: z.string(),
|
|
bucket: z.string(),
|
|
status: z.string(),
|
|
createdAt: z.string().datetime(),
|
|
expiresAt: z.string().datetime().nullable(),
|
|
}),
|
|
);
|
|
|
|
const VideoUploadUrlRequestSchema = registry.register(
|
|
'VideoUploadUrlRequest',
|
|
z.object({
|
|
fileName: z.string().min(1).max(255),
|
|
deviceId: z.string().uuid(),
|
|
prefix: z.string().optional(),
|
|
}),
|
|
);
|
|
|
|
const AdminUploadUrlRequestSchema = registry.register(
|
|
'AdminUploadUrlRequest',
|
|
z.object({
|
|
fileName: z.string().min(1).max(255),
|
|
prefix: z.string().optional(),
|
|
}),
|
|
);
|
|
|
|
const UploadUrlResponseSchema = registry.register(
|
|
'UploadUrlResponse',
|
|
z.object({
|
|
message: z.string(),
|
|
bucket: z.string(),
|
|
objectKey: z.string(),
|
|
uploadUrl: z.string().url(),
|
|
expiresInSeconds: z.number().int().positive(),
|
|
video: VideoRecordSchema,
|
|
}),
|
|
);
|
|
|
|
const DownloadUrlQuerySchema = registry.register(
|
|
'DownloadUrlQuery',
|
|
z.object({
|
|
objectKey: z.string().min(1),
|
|
}),
|
|
);
|
|
|
|
const DownloadUrlResponseSchema = registry.register(
|
|
'DownloadUrlResponse',
|
|
z.object({
|
|
message: z.string(),
|
|
bucket: z.string(),
|
|
objectKey: z.string(),
|
|
downloadUrl: z.string().url(),
|
|
expiresInSeconds: z.number().int().positive(),
|
|
}),
|
|
);
|
|
|
|
const ListVideosQuerySchema = registry.register(
|
|
'ListVideosQuery',
|
|
z.object({
|
|
prefix: z.string().optional(),
|
|
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
}),
|
|
);
|
|
|
|
const ListedObjectSchema = registry.register(
|
|
'ListedObject',
|
|
z.object({
|
|
objectKey: z.string().nullable(),
|
|
size: z.number(),
|
|
etag: z.string().nullable(),
|
|
lastModified: z.string().datetime().nullable(),
|
|
}),
|
|
);
|
|
|
|
const ListVideosResponseSchema = registry.register(
|
|
'ListVideosResponse',
|
|
z.object({
|
|
bucket: z.string(),
|
|
count: z.number().int().nonnegative(),
|
|
objects: z.array(ListedObjectSchema),
|
|
}),
|
|
);
|
|
|
|
const DeleteVideoResponseSchema = registry.register(
|
|
'DeleteVideoResponse',
|
|
z.object({
|
|
message: z.string(),
|
|
bucket: z.string(),
|
|
objectKey: z.string(),
|
|
}),
|
|
);
|
|
|
|
registry.registerPath({
|
|
method: 'get',
|
|
path: '/',
|
|
summary: 'Health check',
|
|
tags: ['System'],
|
|
responses: {
|
|
200: {
|
|
description: 'Service health text',
|
|
content: {
|
|
'text/plain': {
|
|
schema: z.string(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: 'post',
|
|
path: '/videos/upload-url',
|
|
summary: 'Create a presigned upload URL',
|
|
tags: ['Videos'],
|
|
security: [{ cookieAuth: [] }],
|
|
request: {
|
|
body: {
|
|
content: {
|
|
'application/json': {
|
|
schema: VideoUploadUrlRequestSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
201: {
|
|
description: 'Presigned upload URL generated',
|
|
content: {
|
|
'application/json': {
|
|
schema: UploadUrlResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
400: {
|
|
description: 'Invalid request body',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: {
|
|
description: 'Unauthorized',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: 'get',
|
|
path: '/videos/download-url',
|
|
summary: 'Create a signed download URL',
|
|
tags: ['Videos'],
|
|
security: [{ cookieAuth: [] }],
|
|
request: {
|
|
query: DownloadUrlQuerySchema,
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: 'Signed download URL generated',
|
|
content: {
|
|
'application/json': {
|
|
schema: DownloadUrlResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
400: {
|
|
description: 'Invalid query params',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: {
|
|
description: 'Unauthorized',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: 'get',
|
|
path: '/videos',
|
|
summary: 'List objects in the configured bucket',
|
|
tags: ['Videos'],
|
|
security: [{ cookieAuth: [] }],
|
|
request: {
|
|
query: ListVideosQuerySchema,
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: 'Object listing',
|
|
content: {
|
|
'application/json': {
|
|
schema: ListVideosResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
400: {
|
|
description: 'Invalid query params',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: {
|
|
description: 'Unauthorized',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: 'delete',
|
|
path: '/videos',
|
|
summary: 'Delete object by object key',
|
|
tags: ['Videos'],
|
|
security: [{ cookieAuth: [] }],
|
|
request: {
|
|
query: DownloadUrlQuerySchema,
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: 'Object deleted',
|
|
content: {
|
|
'application/json': {
|
|
schema: DeleteVideoResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
400: {
|
|
description: 'Invalid query params',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: {
|
|
description: 'Unauthorized',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: 'post',
|
|
path: '/admin/upload-url',
|
|
summary: 'Admin: create presigned upload URL',
|
|
tags: ['Admin'],
|
|
security: [{ basicAuth: [] }],
|
|
request: {
|
|
body: {
|
|
content: {
|
|
'application/json': {
|
|
schema: AdminUploadUrlRequestSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
201: {
|
|
description: 'Admin upload URL generated',
|
|
content: {
|
|
'application/json': {
|
|
schema: z.object({
|
|
message: z.string(),
|
|
bucket: z.string(),
|
|
objectKey: z.string(),
|
|
uploadUrl: z.string().url(),
|
|
expiresAt: z.string().datetime(),
|
|
expiresInSeconds: z.number().int().positive(),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
400: {
|
|
description: 'Invalid request body',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: {
|
|
description: 'Unauthorized',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: 'get',
|
|
path: '/admin/objects',
|
|
summary: 'Admin: list objects in bucket',
|
|
tags: ['Admin'],
|
|
security: [{ basicAuth: [] }],
|
|
request: {
|
|
query: ListVideosQuerySchema,
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: 'Admin object listing',
|
|
content: {
|
|
'application/json': {
|
|
schema: ListVideosResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
400: {
|
|
description: 'Invalid query params',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: {
|
|
description: 'Unauthorized',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: 'delete',
|
|
path: '/admin/object',
|
|
summary: 'Admin: delete object by key',
|
|
tags: ['Admin'],
|
|
security: [{ basicAuth: [] }],
|
|
request: {
|
|
query: DownloadUrlQuerySchema,
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: 'Object deleted',
|
|
content: {
|
|
'application/json': {
|
|
schema: DeleteVideoResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
400: {
|
|
description: 'Invalid query params',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: {
|
|
description: 'Unauthorized',
|
|
content: {
|
|
'application/json': {
|
|
schema: ErrorResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
export function buildOpenApiDocument() {
|
|
const generator = new OpenApiGeneratorV3(registry.definitions);
|
|
const document = generator.generateDocument({
|
|
openapi: '3.0.3',
|
|
info: {
|
|
title: 'Backend API',
|
|
version: '1.0.0',
|
|
description: 'Auto-generated API documentation from Zod schemas.',
|
|
},
|
|
servers: [{ url: process.env.BETTER_AUTH_URL ?? 'http://localhost:3000' }],
|
|
tags: [
|
|
{ name: 'System', description: 'Service endpoints' },
|
|
{ name: 'Videos', description: 'Authenticated video object operations' },
|
|
{ name: 'Admin', description: 'Basic-auth protected admin operations' },
|
|
],
|
|
});
|
|
|
|
document.components = {
|
|
...(document.components ?? {}),
|
|
securitySchemes: {
|
|
cookieAuth: {
|
|
type: 'apiKey',
|
|
in: 'cookie',
|
|
name: 'better-auth.session_token',
|
|
description: 'Better Auth session cookie',
|
|
},
|
|
basicAuth: {
|
|
type: 'http',
|
|
scheme: 'basic',
|
|
},
|
|
},
|
|
};
|
|
|
|
return document;
|
|
}
|