feat: Implement device-specific video uploads and generate comprehensive OpenAPI documentation.

This commit is contained in:
2025-12-21 13:10:00 +00:00
parent e18f6566e7
commit cdaab7f0c1
7 changed files with 492 additions and 2 deletions

437
Backend/docs/openapi.ts Normal file
View File

@@ -0,0 +1,437 @@
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);
return 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' },
],
components: {
securitySchemes: {
cookieAuth: {
type: 'apiKey',
in: 'cookie',
name: 'better-auth.session_token',
description: 'Better Auth session cookie',
},
basicAuth: {
type: 'http',
scheme: 'basic',
},
},
},
});
}