From 51c51582ac836a5e1276fa280d7612aee1da4400 Mon Sep 17 00:00:00 2001 From: Rajat Date: Thu, 5 Feb 2026 23:50:55 +0530 Subject: [PATCH 1/7] Swagger Docs and OpenAPI 3.0 spec --- AGENTS.md | 4 + apps/api/AGENTS.md | 4 + apps/api/__tests__/media/handlers.test.ts | 12 +- apps/api/package.json | 19 +- apps/api/src/apikey/middleware.ts | 2 +- apps/api/src/index.ts | 124 +++- apps/api/src/media-settings/handlers.ts | 10 +- apps/api/src/media-settings/routes.ts | 55 +- apps/api/src/media-settings/schemas.ts | 17 + apps/api/src/media-settings/service.ts | 14 +- apps/api/src/media/handlers.ts | 15 +- apps/api/src/media/routes.ts | 200 +++++- apps/api/src/media/schemas.ts | 48 ++ apps/api/src/media/service.ts | 3 +- apps/api/src/signature/routes.ts | 23 +- apps/api/src/signature/schemas.ts | 7 + apps/api/src/swagger-generator.ts | 78 +++ apps/api/src/swagger_output.json | 727 ++++++++++++++++++++++ apps/api/tsconfig.json | 11 +- pnpm-lock.yaml | 160 +++-- 20 files changed, 1410 insertions(+), 123 deletions(-) create mode 100644 AGENTS.md create mode 100644 apps/api/AGENTS.md create mode 100644 apps/api/src/media-settings/schemas.ts create mode 100644 apps/api/src/media/schemas.ts create mode 100644 apps/api/src/signature/schemas.ts create mode 100644 apps/api/src/swagger-generator.ts create mode 100644 apps/api/src/swagger_output.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..95adfbb1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,4 @@ +# Dev Environment Tips + +- Use `pnpm` as the package manager. +- This is a monorepo, so use `pnpm --filter ` to run commands in specific packages. diff --git a/apps/api/AGENTS.md b/apps/api/AGENTS.md new file mode 100644 index 00000000..d1269486 --- /dev/null +++ b/apps/api/AGENTS.md @@ -0,0 +1,4 @@ +## Development Tips + +- Always stick to industry standards and best practices for maintaining the REST API documentation using swagger and openapi. +- Stick to OpenAPI 3.0.0 specification for the API documentation. diff --git a/apps/api/__tests__/media/handlers.test.ts b/apps/api/__tests__/media/handlers.test.ts index 3076d217..093fd920 100644 --- a/apps/api/__tests__/media/handlers.test.ts +++ b/apps/api/__tests__/media/handlers.test.ts @@ -94,7 +94,17 @@ describe("Media handlers", () => { ); mock.method(mediaService, "getMediaDetails").mock.mockImplementation( - async () => ({ id: "test-media-id" }), + async () => ({ + mediaId: "test-media-id", + originalFileName: "test.jpg", + mimeType: "image/jpeg", + size: 1024, + access: "private", + file: "http://example.com/file.jpg", + thumbnail: "http://example.com/thumb.jpg", + caption: "test caption", + group: "default", + }), ); const response = await uploadMedia(req, res, () => {}); diff --git a/apps/api/package.json b/apps/api/package.json index f6397e20..de73f820 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -25,10 +25,12 @@ "url": "https://github.com/codelitdev/medialit/issues" }, "scripts": { - "build": "tsc", - "dev": "nodemon src/index.ts", + "build": "pnpm run swagger:generate && tsc", + "swagger:generate": "node --import tsx src/swagger-generator.ts", + "dev": "pnpm run swagger:generate && nodemon --exec 'node --env-file=.env --import tsx' src/index.ts", "start": "node dist/src/index.js", - "test": "node --import tsx --test '**/*.test.ts'" + "test": "node --import tsx --test '**/*.test.ts'", + "test:fuzz": "uvx schemathesis run http://127.0.0.1:8000/openapi.json" }, "dependencies": { "@aws-sdk/client-s3": "^3.922.0", @@ -46,26 +48,29 @@ "express": "^4.2.0", "express-fileupload": "^1.5.2", "joi": "^17.6.0", + "joi-to-swagger": "^6.2.0", "mongoose": "^8.19.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", - "pino": "^10.1.0" + "pino": "^10.1.0", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@types/cors": "^2.8.12", "@types/express": "^4.17.20", "@types/express-fileupload": "^1.2.2", + "@types/joi": "^17.2.3", "@types/mongoose": "^5.11.97", "@types/node": "^22.14.1", "@types/passport": "^1.0.7", "@types/passport-jwt": "^3.0.6", + "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^5.17.0", "@typescript-eslint/parser": "^5.17.0", "eslint": "^8.12.0", "nodemon": "^3.1.10", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", + "swagger-autogen": "^2.23.7", "tsx": "^4.20.6", "typescript": "^5.9.3" } -} +} \ No newline at end of file diff --git a/apps/api/src/apikey/middleware.ts b/apps/api/src/apikey/middleware.ts index 278f8dd3..d0ebac8b 100644 --- a/apps/api/src/apikey/middleware.ts +++ b/apps/api/src/apikey/middleware.ts @@ -13,7 +13,7 @@ export default async function apikey( if (!reqKey) { logger.error({}, "API key is missing"); - return res.status(400).json({ error: BAD_REQUEST }); + return res.status(401).json({ error: UNAUTHORISED }); } const apiKey: Apikey | null = await getApiKeyUsingKeyId(reqKey); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 1dd61294..89913464 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -12,6 +12,9 @@ import logger from "./services/log"; import { createUser, findByEmail } from "./user/queries"; import { Apikey, User } from "@medialit/models"; import { createApiKey } from "./apikey/queries"; +import swaggerUi from "swagger-ui-express"; +import swaggerOutput from "./swagger_output.json"; + import { spawn } from "child_process"; import { cleanupTUSUploads } from "./tus/cleanup"; import { cleanupExpiredTempUploads } from "./media/cleanup"; @@ -24,30 +27,115 @@ app.set("trust proxy", process.env.ENABLE_TRUST_PROXY === "true"); app.use(express.json()); -app.get("/health", (req, res) => { - res.status(200).json({ - status: "ok", - uptime: process.uptime(), - }); -}); +app.get( + "/health", + /* + #swagger.summary = 'Status of the server', + #swagger.description = 'Returns the status of the server and uptime' + #swagger.responses[200] = { + description: "OK", + content: { + "application/json": { + schema: { + type: "object", + properties: { + status: { + type: "string", + example: "ok", + }, + uptime: { + type: "number", + example: 12.345, + }, + }, + }, + }, + }, + } + */ + (req, res) => { + res.status(200).json({ + status: "ok", + uptime: process.uptime(), + }); + }, +); + +app.get( + "/openapi.json", + /* #swagger.ignore = true */ + (req, res) => { + res.json(swaggerOutput); + }, +); + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerOutput)); app.use("/settings/media", mediaSettingsRoutes(passport)); app.use("/media/signature", signatureRoutes); app.use("/media", tusRoutes); app.use("/media", mediaRoutes); -app.get("/cleanup/temp", async (req, res) => { - await cleanupExpiredTempUploads(); - res.status(200).json({ - message: "Expired temp uploads cleaned up", - }); -}); -app.get("/cleanup/tus", async (req, res) => { - await cleanupTUSUploads(); - res.status(200).json({ - message: "Expired tus uploads cleaned up", - }); -}); +app.get( + "/cleanup/temp", + /* + #swagger.tags = ['Cleanup'] + #swagger.summary = 'Cleanup expired temp uploads' + #swagger.description = 'Cleanup expired temp uploads' + #swagger.responses[200] = { + description: "OK", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Expired temp uploads cleaned up", + }, + }, + }, + }, + }, + } + */ + async (req, res) => { + await cleanupExpiredTempUploads(); + res.status(200).json({ + message: "Expired temp uploads cleaned up", + }); + }, +); +app.get( + "/cleanup/tus", + /* + #swagger.tags = ['Cleanup'] + #swagger.summary = 'Cleanup expired tus uploads' + #swagger.description = 'Cleanup expired tus uploads' + #swagger.responses[200] = { + description: "OK", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Expired tus uploads cleaned up", + }, + }, + }, + }, + }, + } + */ + async (req, res) => { + await cleanupTUSUploads(); + res.status(200).json({ + message: "Expired tus uploads cleaned up", + }); + }, +); const port = process.env.PORT || 80; diff --git a/apps/api/src/media-settings/handlers.ts b/apps/api/src/media-settings/handlers.ts index 22bd674b..4ed8f244 100644 --- a/apps/api/src/media-settings/handlers.ts +++ b/apps/api/src/media-settings/handlers.ts @@ -1,21 +1,15 @@ -import Joi from "joi"; import { SUCCESS } from "../config/strings"; import logger from "../services/log"; import { updateMediaSettings } from "./queries"; import * as mediaSettingsService from "./service"; +import { mediaSettingsSchema } from "./schemas"; + export async function updateMediaSettingsHandler( req: any, res: any, next: (...args: any[]) => void, ) { - const mediaSettingsSchema = Joi.object({ - useWebP: Joi.boolean(), - webpOutputQuality: Joi.number().min(0).max(100), - thumbnailWidth: Joi.number().positive(), - thumbnailHeight: Joi.number().positive(), - }); - const { useWebP, webpOutputQuality, thumbnailWidth, thumbnailHeight } = req.body; diff --git a/apps/api/src/media-settings/routes.ts b/apps/api/src/media-settings/routes.ts index 9d430c27..05d69172 100644 --- a/apps/api/src/media-settings/routes.ts +++ b/apps/api/src/media-settings/routes.ts @@ -8,9 +8,60 @@ import apikey from "../apikey/middleware"; export default (passport: any) => { const router = express.Router(); - router.post("/create", apikey, updateMediaSettingsHandler); + router.post( + "/create", + /* + #swagger.tags = ['Settings'] + #swagger.summary = 'Update Media Settings' + #swagger.description = 'Update configuration for media processing.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.requestBody = { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/MediaSettingsPayload" } + } + } + } + #swagger.responses[200] = { + description: 'Settings updated successfully', + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { type: "string", example: "success" } + } + } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + apikey, + updateMediaSettingsHandler, + ); - router.post("/get", apikey, getMediaSettingsHandler); + router.post( + "/get", + /* + #swagger.tags = ['Settings'] + #swagger.summary = 'Get Media Settings' + #swagger.description = 'Retrieve current media processing configuration.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.responses[200] = { + description: 'Settings retrieved successfully', + content: { + "application/json": { + schema: { $ref: '#/components/schemas/MediaSettings' } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + apikey, + getMediaSettingsHandler, + ); return router; }; diff --git a/apps/api/src/media-settings/schemas.ts b/apps/api/src/media-settings/schemas.ts new file mode 100644 index 00000000..f4a0a65b --- /dev/null +++ b/apps/api/src/media-settings/schemas.ts @@ -0,0 +1,17 @@ +import Joi from "joi"; + +export interface MediaSettingsResponse { + useWebP: boolean; + webpOutputQuality: number; + thumbnailHeight: number; + thumbnailWidth: number; +} + +export const mediaSettingsSchema = Joi.object({ + useWebP: Joi.boolean(), + webpOutputQuality: Joi.number().min(0).max(100), + thumbnailWidth: Joi.number().positive(), + thumbnailHeight: Joi.number().positive(), +}); + +export const mediaSettingsResponseSchema = mediaSettingsSchema; diff --git a/apps/api/src/media-settings/service.ts b/apps/api/src/media-settings/service.ts index 1bc59076..5556ea07 100644 --- a/apps/api/src/media-settings/service.ts +++ b/apps/api/src/media-settings/service.ts @@ -1,21 +1,21 @@ -import { MediaSettings } from "./model"; import * as queries from "./queries"; +import { MediaSettingsResponse } from "./schemas"; export async function getMediaSettings( userId: string, apikey: string, -): Promise | null> { +): Promise { const mediaSettings = await queries.getMediaSettings(userId, apikey); if (!mediaSettings) { - return {}; + return null; // Return null if not found to match the type } return { - useWebP: mediaSettings.useWebP, - webpOutputQuality: mediaSettings.webpOutputQuality, - thumbnailHeight: mediaSettings.thumbnailHeight, - thumbnailWidth: mediaSettings.thumbnailWidth, + useWebP: mediaSettings.useWebP || false, + webpOutputQuality: mediaSettings.webpOutputQuality || 0, + thumbnailHeight: mediaSettings.thumbnailHeight || 0, + thumbnailWidth: mediaSettings.thumbnailWidth || 0, }; } diff --git a/apps/api/src/media/handlers.ts b/apps/api/src/media/handlers.ts index 79659776..6eb10b30 100644 --- a/apps/api/src/media/handlers.ts +++ b/apps/api/src/media/handlers.ts @@ -16,15 +16,11 @@ import { getMediaCount as getCount, getTotalSpace } from "./queries"; import { getSubscriptionStatus } from "@medialit/models"; import { getSignatureFromReq } from "../signature/utils"; import getMaxFileUploadSize from "./utils/get-max-file-upload-size"; +import { getMediaSchema, uploadMediaSchema } from "./schemas"; function validateUploadOptions(req: Request): Joi.ValidationResult { - const uploadSchema = Joi.object({ - caption: Joi.string().optional().allow(""), - access: Joi.string().valid("public", "private").optional(), - group: Joi.string().optional(), - }); const { caption, access, group } = req.body; - return uploadSchema.validate({ caption, access, group }); + return uploadMediaSchema.validate({ caption, access, group }); } export async function uploadMedia( @@ -84,13 +80,6 @@ export async function getMedia( res: any, next: (...args: any[]) => void, ) { - const getMediaSchema = Joi.object({ - page: Joi.number().positive(), - limit: Joi.number().positive(), - access: Joi.string().valid("public", "private"), - group: Joi.string(), - }); - const { page, limit, access, group } = req.query; const { error } = getMediaSchema.validate({ page, limit, access, group }); diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index c2d72ac7..93bfd283 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -21,9 +21,45 @@ import { getSignatureFromReq } from "../signature/utils"; const router = express.Router(); -router.options("/create", cors()); router.post( "/create", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'Upload Media' + #swagger.description = 'Upload a new media file. Requires an API key in the `x-medialit-apikey` header or a signature in the `x-medialit-signature` header.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.parameters['x-medialit-signature'] = { + in: 'header', + description: 'Upload Signature for secure client-side uploads', + schema: { type: 'string' }, + required: false + } + #swagger.requestBody = { + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + file: { type: "string", format: "binary" }, + caption: { type: "string" }, + access: { type: "string", enum: ["public", "private"] }, + group: { type: "string" } + } + } + } + } + } + #swagger.responses[200] = { + description: 'The created Media object', + content: { + "application/json": { + schema: { $ref: '#/components/schemas/Media' } + } + } + } + #swagger.responses[400] = { description: 'Bad Request' } + #swagger.responses[401] = { description: 'Unauthorized' } + */ cors(), fileUpload({ useTempFiles: true, @@ -48,11 +84,161 @@ router.post( storage, uploadMedia, ); -router.post("/get/count", apikey, getMediaCount); -router.post("/get/size", apikey, getTotalSpaceOccupied); -router.post("/get/:mediaId", apikey, getMediaDetails); -router.post("/get", apikey, getMedia); -router.post("/seal/:mediaId", apikey, sealMedia); -router.delete("/delete/:mediaId", apikey, deleteMedia); + +router.options("/create", /* #swagger.ignore = true */ cors()); + +router.post( + "/get/count", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'Get Media Count' + #swagger.description = 'Get the total number of media files.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.responses[200] = { + description: 'Count retrieved successfully', + content: { + "application/json": { + schema: { $ref: '#/components/schemas/MediaCountResponse' } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + apikey, + getMediaCount, +); +router.post( + "/get/size", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'Get Total Size' + #swagger.description = 'Get the total size of all media files in bytes.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.responses[200] = { + description: 'Size retrieved successfully', + content: { + "application/json": { + schema: { $ref: '#/components/schemas/MediaSizeResponse' } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + apikey, + getTotalSpaceOccupied, +); +router.post( + "/get/:mediaId", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'Get Media Details' + #swagger.description = 'Retrieve metadata for a specific media item.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.parameters['mediaId'] = { + in: 'path', + description: 'ID of the media file', + required: true, + schema: { type: 'string' } + } + #swagger.responses[200] = { + description: 'Media details retrieved successfully', + content: { + "application/json": { + schema: { $ref: '#/components/schemas/Media' } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[404] = { description: 'Media not found' } + */ + apikey, + getMediaDetails, +); +router.post( + "/get", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'List Media' + #swagger.description = 'List all media files. POST is used to support authenticated requests and complex filters.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.parameters['query'] = { + in: 'query', + name: 'filters', + schema: { $ref: '#/components/schemas/GetMediaQuery' } + } + #swagger.responses[200] = { + description: 'List retrieved successfully', + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: '#/components/schemas/Media' } + } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + apikey, + getMedia, +); +router.post( + "/seal/:mediaId", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'Seal Media' + #swagger.description = 'Seal a media file (mark as processed/finalized).' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.parameters['mediaId'] = { + in: 'path', + description: 'ID of the media file', + required: true, + schema: { type: 'string' } + } + #swagger.responses[200] = { + description: 'The created Media object', + content: { + "application/json": { + schema: { $ref: '#/components/schemas/Media' } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + apikey, + sealMedia, +); +router.delete( + "/delete/:mediaId", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'Delete Media' + #swagger.description = 'Permanently delete a media file.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.parameters['mediaId'] = { + in: 'path', + description: 'ID of the media file', + required: true, + schema: { type: 'string' } + } + #swagger.responses[200] = { + description: 'Media deleted successfully', + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { type: "string", example: "success" } + } + } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[404] = { description: 'Media not found' } + */ + apikey, + deleteMedia, +); export default router; diff --git a/apps/api/src/media/schemas.ts b/apps/api/src/media/schemas.ts new file mode 100644 index 00000000..8c2588b1 --- /dev/null +++ b/apps/api/src/media/schemas.ts @@ -0,0 +1,48 @@ +import Joi from "joi"; +import { AccessControl } from "@medialit/models"; + +export const uploadMediaSchema = Joi.object({ + caption: Joi.string().optional().allow(""), + access: Joi.string().valid("public", "private").optional(), + group: Joi.string().optional(), +}); + +export const getMediaSchema = Joi.object({ + page: Joi.number().positive(), + limit: Joi.number().positive(), + access: Joi.string().valid("public", "private"), + group: Joi.string(), +}); + +export interface MediaResponse { + mediaId: string; + originalFileName: string; + mimeType: string; + size: number; + access: AccessControl; + file: string; + thumbnail: string; + caption?: string; + group?: string; +} + +export const mediaResponseSchema = Joi.object({ + mediaId: Joi.string().required(), + originalFileName: Joi.string().required(), + mimeType: Joi.string().required(), + size: Joi.number().required(), + access: Joi.string().valid("public", "private").required(), + file: Joi.string().uri().required(), + thumbnail: Joi.string().uri().required(), + caption: Joi.string().optional().allow(""), + group: Joi.string().optional(), +}); + +export const mediaCountResponseSchema = Joi.object({ + count: Joi.number().required(), +}); + +export const mediaSizeResponseSchema = Joi.object({ + storage: Joi.number().required(), + maxStorage: Joi.number().required(), +}); diff --git a/apps/api/src/media/service.ts b/apps/api/src/media/service.ts index e697cbf2..e86cca13 100644 --- a/apps/api/src/media/service.ts +++ b/apps/api/src/media/service.ts @@ -43,6 +43,7 @@ import * as presignedUrlService from "../signature/service"; import getTags from "./utils/get-tags"; import { getPublicFileUrl, getThumbnailUrl } from "./utils/get-public-urls"; import { AccessControl, Constants, MediaWithUserId } from "@medialit/models"; +import { MediaResponse } from "./schemas"; const generateAndUploadThumbnail = async ({ workingDirectory, @@ -242,7 +243,7 @@ async function getMediaDetails({ userId: string; apikey: string; mediaId: string; -}): Promise | null> { +}): Promise { const media: MediaWithUserId | null = await getMedia({ userId, apikey, diff --git a/apps/api/src/signature/routes.ts b/apps/api/src/signature/routes.ts index 018c8115..bb8a6574 100644 --- a/apps/api/src/signature/routes.ts +++ b/apps/api/src/signature/routes.ts @@ -3,6 +3,27 @@ import apikey from "../apikey/middleware"; import { getSignature } from "./handlers"; const router = express.Router(); -router.post("/create", apikey, getSignature); +router.post( + "/create", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'Create Upload Signature' + #swagger.description = 'Generate a signature for secure client-side uploads.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.responses[200] = { + description: 'Signature generated successfully', + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/SignatureResponse" + } + } + } + } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + apikey, + getSignature, +); export default router; diff --git a/apps/api/src/signature/schemas.ts b/apps/api/src/signature/schemas.ts new file mode 100644 index 00000000..3bad1ed4 --- /dev/null +++ b/apps/api/src/signature/schemas.ts @@ -0,0 +1,7 @@ +import Joi from "joi"; + +export const signatureResponseSchema = Joi.object({ + signature: Joi.string() + .required() + .description("HMAC signature for secure uploads"), +}); diff --git a/apps/api/src/swagger-generator.ts b/apps/api/src/swagger-generator.ts new file mode 100644 index 00000000..8bc00949 --- /dev/null +++ b/apps/api/src/swagger-generator.ts @@ -0,0 +1,78 @@ +import swaggerAutogen from "swagger-autogen"; +import joiToSwagger from "joi-to-swagger"; +import path from "path"; +import fs from "fs"; + +import { + uploadMediaSchema, + getMediaSchema, + mediaResponseSchema, + mediaCountResponseSchema, + mediaSizeResponseSchema, +} from "./media/schemas"; +import { + mediaSettingsSchema, + mediaSettingsResponseSchema, +} from "./media-settings/schemas"; +import { signatureResponseSchema } from "./signature/schemas"; + +const doc = { + openapi: "3.0.0", + info: { + title: "Medialit API", + description: "Easy file uploads for serverless apps", + version: "0.3.0", + }, + servers: [ + { + url: "{protocol}://{host}", + description: "API Server", + variables: { + protocol: { + default: "https", + enum: ["https", "http"], + }, + host: { + default: "api.medialit.cloud", + }, + }, + }, + ], + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + in: "header", + name: "x-medialit-apikey", + }, + }, + schemas: {}, // Leave empty for autogen, verify later + }, +}; + +const outputFile = path.join(__dirname, "swagger_output.json"); +const routes = [path.join(__dirname, "index.ts")]; + +swaggerAutogen()(outputFile, routes, doc).then(() => { + // Post-process to inject Joi schemas directly (avoiding autogen inference) + const content = JSON.parse(fs.readFileSync(outputFile, "utf8")); + + // Inject components.schemas manually + content.components.schemas = { + ...content.components.schemas, + Media: joiToSwagger(mediaResponseSchema).swagger, + MediaSettings: joiToSwagger(mediaSettingsResponseSchema).swagger, + MediaSettingsPayload: joiToSwagger(mediaSettingsSchema).swagger, + UploadMediaPayload: joiToSwagger(uploadMediaSchema).swagger, + GetMediaQuery: joiToSwagger(getMediaSchema).swagger, + SignatureResponse: joiToSwagger(signatureResponseSchema).swagger, + MediaCountResponse: joiToSwagger(mediaCountResponseSchema).swagger, + MediaSizeResponse: joiToSwagger(mediaSizeResponseSchema).swagger, + }; + + if (content.openapi && content.swagger) { + delete content.swagger; + } + + fs.writeFileSync(outputFile, JSON.stringify(content, null, 2)); +}); diff --git a/apps/api/src/swagger_output.json b/apps/api/src/swagger_output.json new file mode 100644 index 00000000..6506e346 --- /dev/null +++ b/apps/api/src/swagger_output.json @@ -0,0 +1,727 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Medialit API", + "description": "Easy file uploads for serverless apps", + "version": "0.3.0" + }, + "servers": [ + { + "url": "{protocol}://{host}", + "description": "API Server", + "variables": { + "protocol": { + "default": "https", + "enum": [ + "https", + "http" + ] + }, + "host": { + "default": "api.medialit.cloud" + } + } + } + ], + "paths": { + "/health": { + "get": { + "summary": "Status of the server", + "description": "Returns the status of the server and uptime", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "uptime": { + "type": "number", + "example": 12.345 + } + } + } + } + } + } + } + } + }, + "/cleanup/temp": { + "get": { + "tags": [ + "Cleanup" + ], + "summary": "Cleanup expired temp uploads", + "description": "Cleanup expired temp uploads", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Expired temp uploads cleaned up" + } + } + } + } + } + } + } + } + }, + "/cleanup/tus": { + "get": { + "tags": [ + "Cleanup" + ], + "summary": "Cleanup expired tus uploads", + "description": "Cleanup expired tus uploads", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Expired tus uploads cleaned up" + } + } + } + } + } + } + } + } + }, + "/settings/media/create": { + "post": { + "tags": [ + "Settings" + ], + "summary": "Update Media Settings", + "description": "Update configuration for media processing.", + "responses": { + "200": { + "description": "Settings updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "success" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSettingsPayload" + } + } + } + } + } + }, + "/settings/media/get": { + "post": { + "tags": [ + "Settings" + ], + "summary": "Get Media Settings", + "description": "Retrieve current media processing configuration.", + "responses": { + "200": { + "description": "Settings retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSettings" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/media/signature/create": { + "post": { + "tags": [ + "Media" + ], + "summary": "Create Upload Signature", + "description": "Generate a signature for secure client-side uploads.", + "responses": { + "200": { + "description": "Signature generated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignatureResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/media/create": { + "post": { + "tags": [ + "Media" + ], + "summary": "Upload Media", + "description": "Upload a new media file. Requires an API key in the `x-medialit-apikey` header or a signature in the `x-medialit-signature` header.", + "parameters": [ + { + "name": "x-medialit-signature", + "in": "header", + "description": "Upload Signature for secure client-side uploads", + "schema": { + "type": "string" + }, + "required": false + } + ], + "responses": { + "200": { + "description": "The created Media object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Media" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "caption": { + "type": "string" + }, + "access": { + "type": "string", + "enum": [ + "public", + "private" + ] + }, + "group": { + "type": "string" + } + } + } + } + } + } + } + }, + "/media/get/count": { + "post": { + "tags": [ + "Media" + ], + "summary": "Get Media Count", + "description": "Get the total number of media files.", + "responses": { + "200": { + "description": "Count retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaCountResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/media/get/size": { + "post": { + "tags": [ + "Media" + ], + "summary": "Get Total Size", + "description": "Get the total size of all media files in bytes.", + "responses": { + "200": { + "description": "Size retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSizeResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/media/get/{mediaId}": { + "post": { + "tags": [ + "Media" + ], + "summary": "Get Media Details", + "description": "Retrieve metadata for a specific media item.", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the media file", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Media details retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Media" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Media not found" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/media/get": { + "post": { + "tags": [ + "Media" + ], + "summary": "List Media", + "description": "List all media files. POST is used to support authenticated requests and complex filters.", + "parameters": [ + { + "name": "filters", + "in": "query", + "schema": { + "$ref": "#/components/schemas/GetMediaQuery" + } + } + ], + "responses": { + "200": { + "description": "List retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Media" + } + } + } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/media/seal/{mediaId}": { + "post": { + "tags": [ + "Media" + ], + "summary": "Seal Media", + "description": "Seal a media file (mark as processed/finalized).", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the media file", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The created Media object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Media" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/media/delete/{mediaId}": { + "delete": { + "tags": [ + "Media" + ], + "summary": "Delete Media", + "description": "Permanently delete a media file.", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the media file", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Media deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "success" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Media not found" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "x-medialit-apikey" + } + }, + "schemas": { + "Media": { + "type": "object", + "properties": { + "mediaId": { + "type": "string" + }, + "originalFileName": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "number", + "format": "float" + }, + "access": { + "type": "string", + "enum": [ + "public", + "private" + ] + }, + "file": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "group": { + "type": "string" + } + }, + "required": [ + "mediaId", + "originalFileName", + "mimeType", + "size", + "access", + "file", + "thumbnail" + ], + "additionalProperties": false + }, + "MediaSettings": { + "type": "object", + "properties": { + "useWebP": { + "type": "boolean" + }, + "webpOutputQuality": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 100 + }, + "thumbnailWidth": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "thumbnailHeight": { + "type": "number", + "format": "float", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "MediaSettingsPayload": { + "type": "object", + "properties": { + "useWebP": { + "type": "boolean" + }, + "webpOutputQuality": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 100 + }, + "thumbnailWidth": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "thumbnailHeight": { + "type": "number", + "format": "float", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "UploadMediaPayload": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "access": { + "type": "string", + "enum": [ + "public", + "private" + ] + }, + "group": { + "type": "string" + } + }, + "additionalProperties": false + }, + "GetMediaQuery": { + "type": "object", + "properties": { + "page": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "limit": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "access": { + "type": "string", + "enum": [ + "public", + "private" + ] + }, + "group": { + "type": "string" + } + }, + "additionalProperties": false + }, + "SignatureResponse": { + "type": "object", + "properties": { + "signature": { + "type": "string", + "description": "HMAC signature for secure uploads" + } + }, + "required": [ + "signature" + ], + "additionalProperties": false + }, + "MediaCountResponse": { + "type": "object", + "properties": { + "count": { + "type": "number", + "format": "float" + } + }, + "required": [ + "count" + ], + "additionalProperties": false + }, + "MediaSizeResponse": { + "type": "object", + "properties": { + "storage": { + "type": "number", + "format": "float" + }, + "maxStorage": { + "type": "number", + "format": "float" + } + }, + "required": [ + "storage", + "maxStorage" + ], + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index de7833dc..42bb5f39 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,8 +1,4 @@ { - "ts-node": { - "files": true, - "require": ["tsconfig-paths/register"] - }, "compilerOptions": { "outDir": "dist", "target": "es5", @@ -11,9 +7,12 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 016f9d50..b3e72862 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: joi: specifier: ^17.6.0 version: 17.13.3 + joi-to-swagger: + specifier: ^6.2.0 + version: 6.2.0(joi@17.13.3) mongoose: specifier: ^8.19.3 version: 8.19.3 @@ -107,6 +110,9 @@ importers: pino: specifier: ^10.1.0 version: 10.1.0 + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@4.21.2) devDependencies: '@types/cors': specifier: ^2.8.12 @@ -117,6 +123,9 @@ importers: '@types/express-fileupload': specifier: ^1.2.2 version: 1.5.1 + '@types/joi': + specifier: ^17.2.3 + version: 17.2.3 '@types/mongoose': specifier: ^5.11.97 version: 5.11.97 @@ -129,6 +138,9 @@ importers: '@types/passport-jwt': specifier: ^3.0.6 version: 3.0.13 + '@types/swagger-ui-express': + specifier: ^4.1.8 + version: 4.1.8 '@typescript-eslint/eslint-plugin': specifier: ^5.17.0 version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -141,12 +153,9 @@ importers: nodemon: specifier: ^3.1.10 version: 3.1.10 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.14.1)(typescript@5.9.3) - tsconfig-paths: - specifier: ^4.2.0 - version: 4.2.0 + swagger-autogen: + specifier: ^2.23.7 + version: 2.23.7 tsx: specifier: ^4.20.6 version: 4.20.6 @@ -2236,6 +2245,9 @@ packages: '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@shikijs/core@3.3.0': resolution: {integrity: sha512-CovkFL2WVaHk6PCrwv6ctlmD4SS1qtIfN8yEyDXDYWh4ONvomdM9MaFw20qHuqJOcb8/xrkqoWQRJ//X10phOQ==} @@ -2669,6 +2681,10 @@ packages: '@types/istanbul-reports@1.1.2': resolution: {integrity: sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==} + '@types/joi@17.2.3': + resolution: {integrity: sha512-dGjs/lhrWOa+eO0HwgxCSnDm5eMGCsXuvLglMghJq32F6q5LyyNuXb41DHzrg501CKNOSSAHmfB7FDGeUnDmzw==} + deprecated: This is a stub types definition. joi provides its own type definitions, so you do not need this installed. + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2749,6 +2765,9 @@ packages: '@types/stack-utils@1.0.1': resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==} + '@types/swagger-ui-express@4.1.8': + resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2984,6 +3003,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.14.1: resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} @@ -3602,6 +3626,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -4336,11 +4364,12 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -4939,6 +4968,12 @@ packages: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} engines: {node: '>= 0.6.0'} + joi-to-swagger@6.2.0: + resolution: {integrity: sha512-gwfIr1TsbbvZWozB/sFqiD7POFcXeaLKp6QJKGFkVgdom2ie/4f75QQAanZc/Wlbnyk66e6kTZXO28i6pN3oQA==} + engines: {node: '>=10.0.0'} + peerDependencies: + joi: '>=17.1.1' + joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -5660,6 +5695,7 @@ packages: next@15.5.7: resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -6871,6 +6907,18 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-autogen@2.23.7: + resolution: {integrity: sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==} + + swagger-ui-dist@5.31.0: + resolution: {integrity: sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -7006,10 +7054,6 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} - tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -8570,7 +8614,9 @@ snapshots: source-map: 0.6.1 string-length: 2.0.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate '@jest/source-map@24.9.0': dependencies: @@ -9678,6 +9724,8 @@ snapshots: '@rushstack/eslint-patch@1.11.0': {} + '@scarf/scarf@1.4.0': {} + '@shikijs/core@3.3.0': dependencies: '@shikijs/types': 3.3.0 @@ -10266,6 +10314,10 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-lib-report': 3.0.3 + '@types/joi@17.2.3': + dependencies: + joi: 17.13.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -10361,6 +10413,11 @@ snapshots: '@types/stack-utils@1.0.1': {} + '@types/swagger-ui-express@4.1.8': + dependencies: + '@types/express': 4.17.21 + '@types/serve-static': 1.15.7 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -10714,6 +10771,8 @@ snapshots: acorn@6.4.2: {} + acorn@7.4.1: {} + acorn@8.14.1: {} ajv@6.12.6: @@ -11354,6 +11413,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -11659,8 +11720,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) @@ -11679,8 +11740,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.24.0(jiti@2.4.2)) @@ -11699,7 +11760,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) @@ -11710,11 +11771,11 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) @@ -11725,33 +11786,33 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11762,7 +11823,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11780,7 +11841,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11791,7 +11852,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13351,6 +13412,11 @@ snapshots: jmespath@0.16.0: {} + joi-to-swagger@6.2.0(joi@17.13.3): + dependencies: + joi: 17.13.3 + lodash: 4.17.21 + joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -15723,6 +15789,22 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-autogen@2.23.7: + dependencies: + acorn: 7.4.1 + deepmerge: 4.3.1 + glob: 7.2.3 + json5: 2.2.3 + + swagger-ui-dist@5.31.0: + dependencies: + '@scarf/scarf': 1.4.0 + + swagger-ui-express@5.0.1(express@4.21.2): + dependencies: + express: 4.21.2 + swagger-ui-dist: 5.31.0 + symbol-tree@3.2.4: {} tailwind-merge@2.6.0: {} @@ -15888,24 +15970,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@22.14.1)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.14.1 - acorn: 8.14.1 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -15913,12 +15977,6 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsconfig-paths@4.2.0: - dependencies: - json5: 2.2.3 - minimist: 1.2.8 - strip-bom: 3.0.0 - tslib@1.14.1: {} tslib@2.8.1: {} From 2ef42a7b2bf03b6e542a2558837a60945b453a4b Mon Sep 17 00:00:00 2001 From: Rajat Date: Fri, 6 Feb 2026 11:28:09 +0530 Subject: [PATCH 2/7] Updated docs --- apps/docs/content/docs/rest-api.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/docs/rest-api.mdx b/apps/docs/content/docs/rest-api.mdx index 4979b90d..e37b68d0 100644 --- a/apps/docs/content/docs/rest-api.mdx +++ b/apps/docs/content/docs/rest-api.mdx @@ -5,7 +5,7 @@ description: Upload files in under five minutes with our API ## API Documentation -Access the complete endpoint reference through our official [Postman](https://www.postman.com/codelitdev/codelit/collection/5b8hfkr/medialit) collection. +Our API is built on top of OpenAPI 3.0 and is available [here](https://api.medialit.cloud/docs). ## Video Tutorial From 46e23c09fd649842342e7c612b7b6956d7243d4d Mon Sep 17 00:00:00 2001 From: Rajat Date: Thu, 19 Mar 2026 22:17:35 +0530 Subject: [PATCH 3/7] swagger doc fixes --- apps/api/AGENTS.md | 2 +- apps/api/src/index.ts | 59 +--- apps/api/src/media-settings/routes.ts | 3 + apps/api/src/media/handlers.ts | 4 +- apps/api/src/media/routes.ts | 36 +- apps/api/src/signature/handlers.ts | 2 +- apps/api/src/signature/routes.ts | 2 + apps/api/src/swagger-generator.ts | 128 ++++++- apps/api/src/swagger_output.json | 486 +++++++++++++++++++++----- apps/docs/content/docs/rest-api.mdx | 4 +- 10 files changed, 572 insertions(+), 154 deletions(-) diff --git a/apps/api/AGENTS.md b/apps/api/AGENTS.md index d1269486..5be0e951 100644 --- a/apps/api/AGENTS.md +++ b/apps/api/AGENTS.md @@ -1,4 +1,4 @@ ## Development Tips - Always stick to industry standards and best practices for maintaining the REST API documentation using swagger and openapi. -- Stick to OpenAPI 3.0.0 specification for the API documentation. +- Stick to OpenAPI >=3.0.3 specification for the API documentation. diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 89913464..b495544e 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -69,7 +69,20 @@ app.get( }, ); -app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerOutput)); +app.use( + "/docs", + swaggerUi.serve, + swaggerUi.setup(swaggerOutput, { + explorer: true, + swaggerOptions: { + persistAuthorization: true, + displayRequestDuration: true, + docExpansion: "none", + defaultModelsExpandDepth: -1, + validatorUrl: null, + }, + }), +); app.use("/settings/media", mediaSettingsRoutes(passport)); app.use("/media/signature", signatureRoutes); @@ -78,27 +91,7 @@ app.use("/media", mediaRoutes); app.get( "/cleanup/temp", - /* - #swagger.tags = ['Cleanup'] - #swagger.summary = 'Cleanup expired temp uploads' - #swagger.description = 'Cleanup expired temp uploads' - #swagger.responses[200] = { - description: "OK", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Expired temp uploads cleaned up", - }, - }, - }, - }, - }, - } - */ + /* #swagger.ignore = true */ async (req, res) => { await cleanupExpiredTempUploads(); res.status(200).json({ @@ -108,27 +101,7 @@ app.get( ); app.get( "/cleanup/tus", - /* - #swagger.tags = ['Cleanup'] - #swagger.summary = 'Cleanup expired tus uploads' - #swagger.description = 'Cleanup expired tus uploads' - #swagger.responses[200] = { - description: "OK", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Expired tus uploads cleaned up", - }, - }, - }, - }, - }, - } - */ + /* #swagger.ignore = true */ async (req, res) => { await cleanupTUSUploads(); res.status(200).json({ diff --git a/apps/api/src/media-settings/routes.ts b/apps/api/src/media-settings/routes.ts index 05d69172..7cc98070 100644 --- a/apps/api/src/media-settings/routes.ts +++ b/apps/api/src/media-settings/routes.ts @@ -36,7 +36,9 @@ export default (passport: any) => { } } } + #swagger.responses[400] = { description: 'Bad Request' } #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[500] = { description: 'Internal Server Error' } */ apikey, updateMediaSettingsHandler, @@ -58,6 +60,7 @@ export default (passport: any) => { } } #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[500] = { description: 'Internal Server Error' } */ apikey, getMediaSettingsHandler, diff --git a/apps/api/src/media/handlers.ts b/apps/api/src/media/handlers.ts index 6eb10b30..709b2289 100644 --- a/apps/api/src/media/handlers.ts +++ b/apps/api/src/media/handlers.ts @@ -80,7 +80,9 @@ export async function getMedia( res: any, next: (...args: any[]) => void, ) { - const { page, limit, access, group } = req.query; + const filters = + req.body && Object.keys(req.body).length > 0 ? req.body : req.query; + const { page, limit, access, group } = filters; const { error } = getMediaSchema.validate({ page, limit, access, group }); diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index 93bfd283..d1df83c1 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -26,7 +26,7 @@ router.post( /* #swagger.tags = ['Media'] #swagger.summary = 'Upload Media' - #swagger.description = 'Upload a new media file. Requires an API key in the `x-medialit-apikey` header or a signature in the `x-medialit-signature` header.' + #swagger.description = 'Upload a new media file. Use API key auth from Authorize (`x-medialit-apikey`) or pass `x-medialit-signature` for this endpoint only.' #swagger.security = [{ "apiKeyAuth": [] }] #swagger.parameters['x-medialit-signature'] = { in: 'header', @@ -35,6 +35,7 @@ router.post( required: false } #swagger.requestBody = { + required: true, content: { "multipart/form-data": { schema: { @@ -44,7 +45,8 @@ router.post( caption: { type: "string" }, access: { type: "string", enum: ["public", "private"] }, group: { type: "string" } - } + }, + required: ["file"] } } } @@ -59,6 +61,16 @@ router.post( } #swagger.responses[400] = { description: 'Bad Request' } #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[404] = { + description: 'Invalid signature', + content: { + "application/json": { + schema: { $ref: '#/components/schemas/ErrorResponse' }, + example: { error: 'Invalid signature' } + } + } + } + #swagger.responses[500] = { description: 'Internal Server Error' } */ cors(), fileUpload({ @@ -150,6 +162,7 @@ router.post( } #swagger.responses[401] = { description: 'Unauthorized' } #swagger.responses[404] = { description: 'Media not found' } + #swagger.responses[500] = { description: 'Internal Server Error' } */ apikey, getMediaDetails, @@ -159,12 +172,15 @@ router.post( /* #swagger.tags = ['Media'] #swagger.summary = 'List Media' - #swagger.description = 'List all media files. POST is used to support authenticated requests and complex filters.' + #swagger.description = 'List media files filtered by optional query payload.' #swagger.security = [{ "apiKeyAuth": [] }] - #swagger.parameters['query'] = { - in: 'query', - name: 'filters', - schema: { $ref: '#/components/schemas/GetMediaQuery' } + #swagger.requestBody = { + required: false, + content: { + "application/json": { + schema: { $ref: '#/components/schemas/GetMediaQuery' } + } + } } #swagger.responses[200] = { description: 'List retrieved successfully', @@ -177,7 +193,9 @@ router.post( } } } + #swagger.responses[400] = { description: 'Bad Request' } #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[500] = { description: 'Internal Server Error' } */ apikey, getMedia, @@ -204,6 +222,8 @@ router.post( } } #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[404] = { description: 'Media not found' } + #swagger.responses[500] = { description: 'Internal Server Error' } */ apikey, sealMedia, @@ -235,7 +255,7 @@ router.delete( } } #swagger.responses[401] = { description: 'Unauthorized' } - #swagger.responses[404] = { description: 'Media not found' } + #swagger.responses[500] = { description: 'Internal Server Error' } */ apikey, deleteMedia, diff --git a/apps/api/src/signature/handlers.ts b/apps/api/src/signature/handlers.ts index 9ee75761..527e91e8 100644 --- a/apps/api/src/signature/handlers.ts +++ b/apps/api/src/signature/handlers.ts @@ -30,6 +30,6 @@ export async function getSignature( return res.status(200).json({ signature }); } catch (err: any) { logger.error({ err }, err.message); - return res.status(500).json(err.message); + return res.status(500).json({ error: err.message }); } } diff --git a/apps/api/src/signature/routes.ts b/apps/api/src/signature/routes.ts index bb8a6574..1db200bd 100644 --- a/apps/api/src/signature/routes.ts +++ b/apps/api/src/signature/routes.ts @@ -20,7 +20,9 @@ router.post( } } } + #swagger.responses[400] = { description: 'Bad Request' } #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[500] = { description: 'Internal Server Error' } */ apikey, getSignature, diff --git a/apps/api/src/swagger-generator.ts b/apps/api/src/swagger-generator.ts index 8bc00949..b99c57cc 100644 --- a/apps/api/src/swagger-generator.ts +++ b/apps/api/src/swagger-generator.ts @@ -17,11 +17,20 @@ import { import { signatureResponseSchema } from "./signature/schemas"; const doc = { - openapi: "3.0.0", + openapi: "3.0.3", info: { title: "Medialit API", description: "Easy file uploads for serverless apps", version: "0.3.0", + termsOfService: "https://medialit.cloud/p/terms", + contact: { + name: "Medialit Support", + url: "https://medialit.cloud", + }, + license: { + name: "AGPL-3.0", + url: "https://www.gnu.org/licenses/agpl-3.0.en.html", + }, }, servers: [ { @@ -38,6 +47,16 @@ const doc = { }, }, ], + tags: [ + { + name: "Media", + description: "Upload, list, and manage media resources.", + }, + { + name: "Settings", + description: "Manage media processing configuration.", + }, + ], components: { securitySchemes: { apiKeyAuth: { @@ -57,6 +76,28 @@ swaggerAutogen()(outputFile, routes, doc).then(() => { // Post-process to inject Joi schemas directly (avoiding autogen inference) const content = JSON.parse(fs.readFileSync(outputFile, "utf8")); + const operationIdByMethodAndPath: Record = { + "get /health": "getHealth", + "post /settings/media/create": "updateMediaSettings", + "post /settings/media/get": "getMediaSettings", + "post /media/signature/create": "createUploadSignature", + "post /media/create": "uploadMedia", + "post /media/get/count": "getMediaCount", + "post /media/get/size": "getMediaSize", + "post /media/get/{mediaId}": "getMediaDetails", + "post /media/get": "listMedia", + "post /media/seal/{mediaId}": "sealMedia", + "delete /media/delete/{mediaId}": "deleteMedia", + }; + + const errorDescriptionByStatus: Record = { + "400": "Bad Request", + "401": "Unauthorized", + "404": "Not Found", + "409": "Conflict", + "500": "Internal Server Error", + }; + // Inject components.schemas manually content.components.schemas = { ...content.components.schemas, @@ -68,8 +109,93 @@ swaggerAutogen()(outputFile, routes, doc).then(() => { SignatureResponse: joiToSwagger(signatureResponseSchema).swagger, MediaCountResponse: joiToSwagger(mediaCountResponseSchema).swagger, MediaSizeResponse: joiToSwagger(mediaSizeResponseSchema).swagger, + ErrorResponse: { + type: "object", + properties: { + error: { + type: "string", + example: "Unauthorized", + }, + }, + required: ["error"], + additionalProperties: false, + }, }; + if (content.paths) { + delete content.paths["/cleanup/temp"]; + delete content.paths["/cleanup/tus"]; + } + + Object.entries(content.paths || {}).forEach(([apiPath, pathItem]: any) => { + Object.entries(pathItem || {}).forEach(([method, operation]: any) => { + if (!operation || typeof operation !== "object") { + return; + } + + const operationId = + operationIdByMethodAndPath[`${method} ${apiPath}`]; + if (operationId) { + operation.operationId = operationId; + } + + if (Array.isArray(operation.parameters)) { + operation.parameters.forEach((parameter: any) => { + if (parameter && typeof parameter === "object") { + delete parameter.type; + } + }); + } + + if (apiPath === "/media/create" && method === "post") { + operation.security = [{ apiKeyAuth: [] }]; + } + + if (apiPath === "/health" && method === "get") { + operation.security = []; + } + + if (apiPath === "/media/get" && method === "post") { + delete operation.parameters; + operation.requestBody = { + required: false, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/GetMediaQuery", + }, + }, + }, + }; + } + + operation.responses = operation.responses || {}; + ["400", "401", "404", "409", "500"].forEach((statusCode) => { + const existingResponse = operation.responses[statusCode]; + if (!existingResponse || existingResponse.$ref) { + return; + } + + operation.responses[statusCode] = { + ...existingResponse, + description: + existingResponse.description || + errorDescriptionByStatus[statusCode], + content: existingResponse.content || { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + example: { + error: errorDescriptionByStatus[statusCode], + }, + }, + }, + }; + }); + }); + }); + if (content.openapi && content.swagger) { delete content.swagger; } diff --git a/apps/api/src/swagger_output.json b/apps/api/src/swagger_output.json index 6506e346..371b8e2e 100644 --- a/apps/api/src/swagger_output.json +++ b/apps/api/src/swagger_output.json @@ -1,9 +1,18 @@ { - "openapi": "3.0.0", + "openapi": "3.0.3", "info": { "title": "Medialit API", "description": "Easy file uploads for serverless apps", - "version": "0.3.0" + "version": "0.3.0", + "termsOfService": "https://medialit.cloud/p/terms", + "contact": { + "name": "Medialit Support", + "url": "https://medialit.cloud" + }, + "license": { + "name": "AGPL-3.0", + "url": "https://www.gnu.org/licenses/agpl-3.0.en.html" + } }, "servers": [ { @@ -23,6 +32,16 @@ } } ], + "tags": [ + { + "name": "Media", + "description": "Upload, list, and manage media resources." + }, + { + "name": "Settings", + "description": "Manage media processing configuration." + } + ], "paths": { "/health": { "get": { @@ -49,19 +68,21 @@ } } } - } + }, + "operationId": "getHealth", + "security": [] } }, - "/cleanup/temp": { - "get": { + "/settings/media/create": { + "post": { "tags": [ - "Cleanup" + "Settings" ], - "summary": "Cleanup expired temp uploads", - "description": "Cleanup expired temp uploads", + "summary": "Update Media Settings", + "description": "Update configuration for media processing.", "responses": { "200": { - "description": "OK", + "description": "Settings updated successfully", "content": { "application/json": { "schema": { @@ -69,69 +90,51 @@ "properties": { "message": { "type": "string", - "example": "Expired temp uploads cleaned up" + "example": "success" } } } } } - } - } - } - }, - "/cleanup/tus": { - "get": { - "tags": [ - "Cleanup" - ], - "summary": "Cleanup expired tus uploads", - "description": "Cleanup expired tus uploads", - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "example": "Expired tus uploads cleaned up" - } - } + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" } } } - } - } - } - }, - "/settings/media/create": { - "post": { - "tags": [ - "Settings" - ], - "summary": "Update Media Settings", - "description": "Update configuration for media processing.", - "responses": { - "200": { - "description": "Settings updated successfully", + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "example": "success" - } - } + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" } } } }, - "401": { - "description": "Unauthorized" + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ @@ -148,7 +151,8 @@ } } } - } + }, + "operationId": "updateMediaSettings" } }, "/settings/media/get": { @@ -170,17 +174,38 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } }, "500": { - "description": "Internal Server Error" + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "operationId": "getMediaSettings" } }, "/media/signature/create": { @@ -201,15 +226,52 @@ } } }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "operationId": "createUploadSignature" } }, "/media/create": { @@ -218,7 +280,7 @@ "Media" ], "summary": "Upload Media", - "description": "Upload a new media file. Requires an API key in the `x-medialit-apikey` header or a signature in the `x-medialit-signature` header.", + "description": "Upload a new media file. Use API key auth from Authorize (`x-medialit-apikey`) or pass `x-medialit-signature` for this endpoint only.", "parameters": [ { "name": "x-medialit-signature", @@ -242,10 +304,56 @@ } }, "400": { - "description": "Bad Request" + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "404": { + "description": "Invalid signature", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Invalid signature" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ @@ -254,6 +362,7 @@ } ], "requestBody": { + "required": true, "content": { "multipart/form-data": { "schema": { @@ -276,11 +385,15 @@ "group": { "type": "string" } - } + }, + "required": [ + "file" + ] } } } - } + }, + "operationId": "uploadMedia" } }, "/media/get/count": { @@ -302,17 +415,38 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } }, "500": { - "description": "Internal Server Error" + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "operationId": "getMediaCount" } }, "/media/get/size": { @@ -334,17 +468,38 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } }, "500": { - "description": "Internal Server Error" + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "operationId": "getMediaSize" } }, "/media/get/{mediaId}": { @@ -359,7 +514,6 @@ "name": "mediaId", "in": "path", "required": true, - "type": "string", "description": "ID of the media file", "schema": { "type": "string" @@ -378,17 +532,51 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } }, "404": { - "description": "Media not found" + "description": "Media not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Not Found" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "operationId": "getMediaDetails" } }, "/media/get": { @@ -397,16 +585,7 @@ "Media" ], "summary": "List Media", - "description": "List all media files. POST is used to support authenticated requests and complex filters.", - "parameters": [ - { - "name": "filters", - "in": "query", - "schema": { - "$ref": "#/components/schemas/GetMediaQuery" - } - } - ], + "description": "List media files filtered by optional query payload.", "responses": { "200": { "description": "List retrieved successfully", @@ -421,15 +600,62 @@ } } }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMediaQuery" + } + } + } + }, + "operationId": "listMedia" } }, "/media/seal/{mediaId}": { @@ -444,7 +670,6 @@ "name": "mediaId", "in": "path", "required": true, - "type": "string", "description": "ID of the media file", "schema": { "type": "string" @@ -463,14 +688,51 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "404": { + "description": "Media not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Not Found" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "operationId": "sealMedia" } }, "/media/delete/{mediaId}": { @@ -485,7 +747,6 @@ "name": "mediaId", "in": "path", "required": true, - "type": "string", "description": "ID of the media file", "schema": { "type": "string" @@ -510,20 +771,38 @@ } }, "401": { - "description": "Unauthorized" - }, - "404": { - "description": "Media not found" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } }, "500": { - "description": "Internal Server Error" + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } } }, "security": [ { "apiKeyAuth": [] } - ] + ], + "operationId": "deleteMedia" } } }, @@ -721,6 +1000,19 @@ "maxStorage" ], "additionalProperties": false + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + }, + "required": [ + "error" + ], + "additionalProperties": false } } } diff --git a/apps/docs/content/docs/rest-api.mdx b/apps/docs/content/docs/rest-api.mdx index e37b68d0..c45dcf8f 100644 --- a/apps/docs/content/docs/rest-api.mdx +++ b/apps/docs/content/docs/rest-api.mdx @@ -5,8 +5,8 @@ description: Upload files in under five minutes with our API ## API Documentation -Our API is built on top of OpenAPI 3.0 and is available [here](https://api.medialit.cloud/docs). +Our API is built on top of OpenAPI 3.0.3 and is available [here](https://api.medialit.cloud/docs). ## Video Tutorial - \ No newline at end of file + From e5c35c61aed9cf5128578693511cabb562ea367f Mon Sep 17 00:00:00 2001 From: Rajat Date: Thu, 19 Mar 2026 23:18:57 +0530 Subject: [PATCH 4/7] Coded recommended changes --- apps/api/src/media-settings/service.ts | 18 ++++++++++++++---- apps/api/src/media/schemas.ts | 2 +- apps/api/src/swagger-generator.ts | 4 ---- apps/api/src/swagger_output.json | 3 +-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/api/src/media-settings/service.ts b/apps/api/src/media-settings/service.ts index 5556ea07..e6fe61f9 100644 --- a/apps/api/src/media-settings/service.ts +++ b/apps/api/src/media-settings/service.ts @@ -1,21 +1,31 @@ import * as queries from "./queries"; import { MediaSettingsResponse } from "./schemas"; +import { + thumbnailHeight as defaultThumbnailHeight, + thumbnailWidth as defaultThumbnailWidth, +} from "../config/constants"; export async function getMediaSettings( userId: string, apikey: string, -): Promise { +): Promise { const mediaSettings = await queries.getMediaSettings(userId, apikey); if (!mediaSettings) { - return null; // Return null if not found to match the type + return { + useWebP: false, + webpOutputQuality: 0, + thumbnailHeight: defaultThumbnailHeight, + thumbnailWidth: defaultThumbnailWidth, + }; } return { useWebP: mediaSettings.useWebP || false, webpOutputQuality: mediaSettings.webpOutputQuality || 0, - thumbnailHeight: mediaSettings.thumbnailHeight || 0, - thumbnailWidth: mediaSettings.thumbnailWidth || 0, + thumbnailHeight: + mediaSettings.thumbnailHeight || defaultThumbnailHeight, + thumbnailWidth: mediaSettings.thumbnailWidth || defaultThumbnailWidth, }; } diff --git a/apps/api/src/media/schemas.ts b/apps/api/src/media/schemas.ts index 8c2588b1..31be3db5 100644 --- a/apps/api/src/media/schemas.ts +++ b/apps/api/src/media/schemas.ts @@ -33,7 +33,7 @@ export const mediaResponseSchema = Joi.object({ size: Joi.number().required(), access: Joi.string().valid("public", "private").required(), file: Joi.string().uri().required(), - thumbnail: Joi.string().uri().required(), + thumbnail: Joi.string().uri().optional(), caption: Joi.string().optional().allow(""), group: Joi.string().optional(), }); diff --git a/apps/api/src/swagger-generator.ts b/apps/api/src/swagger-generator.ts index b99c57cc..2543c2f8 100644 --- a/apps/api/src/swagger-generator.ts +++ b/apps/api/src/swagger-generator.ts @@ -147,10 +147,6 @@ swaggerAutogen()(outputFile, routes, doc).then(() => { }); } - if (apiPath === "/media/create" && method === "post") { - operation.security = [{ apiKeyAuth: [] }]; - } - if (apiPath === "/health" && method === "get") { operation.security = []; } diff --git a/apps/api/src/swagger_output.json b/apps/api/src/swagger_output.json index 371b8e2e..6b9de5af 100644 --- a/apps/api/src/swagger_output.json +++ b/apps/api/src/swagger_output.json @@ -857,8 +857,7 @@ "mimeType", "size", "access", - "file", - "thumbnail" + "file" ], "additionalProperties": false }, From ca4c324ca1edb33ff87df55a006c11de6d878863 Mon Sep 17 00:00:00 2001 From: Rajat Date: Thu, 19 Mar 2026 23:37:41 +0530 Subject: [PATCH 5/7] Added back signature security scheme --- apps/api/src/media/routes.ts | 2 +- apps/api/src/swagger-generator.ts | 5 +++++ apps/api/src/swagger_output.json | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index d1df83c1..374f7050 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -27,7 +27,7 @@ router.post( #swagger.tags = ['Media'] #swagger.summary = 'Upload Media' #swagger.description = 'Upload a new media file. Use API key auth from Authorize (`x-medialit-apikey`) or pass `x-medialit-signature` for this endpoint only.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "apiKeyAuth": [] }, { "signatureAuth": [] }] #swagger.parameters['x-medialit-signature'] = { in: 'header', description: 'Upload Signature for secure client-side uploads', diff --git a/apps/api/src/swagger-generator.ts b/apps/api/src/swagger-generator.ts index 2543c2f8..e6e1692b 100644 --- a/apps/api/src/swagger-generator.ts +++ b/apps/api/src/swagger-generator.ts @@ -64,6 +64,11 @@ const doc = { in: "header", name: "x-medialit-apikey", }, + signatureAuth: { + type: "apiKey", + in: "header", + name: "x-medialit-signature", + }, }, schemas: {}, // Leave empty for autogen, verify later }, diff --git a/apps/api/src/swagger_output.json b/apps/api/src/swagger_output.json index 6b9de5af..e731d1c0 100644 --- a/apps/api/src/swagger_output.json +++ b/apps/api/src/swagger_output.json @@ -359,6 +359,9 @@ "security": [ { "apiKeyAuth": [] + }, + { + "signatureAuth": [] } ], "requestBody": { @@ -812,6 +815,11 @@ "type": "apiKey", "in": "header", "name": "x-medialit-apikey" + }, + "signatureAuth": { + "type": "apiKey", + "in": "header", + "name": "x-medialit-signature" } }, "schemas": { From 8a04c27669e7c319649d56fbc212a518f2782020 Mon Sep 17 00:00:00 2001 From: Rajat Date: Fri, 20 Mar 2026 19:22:12 +0530 Subject: [PATCH 6/7] Codex suggested code review fixes --- apps/api/src/media/routes.ts | 2 +- apps/api/src/media/schemas.ts | 24 ++++++++++++++++- apps/api/src/swagger-generator.ts | 16 +++++++++++ apps/api/src/swagger_output.json | 44 ++++++++++++++++++++++++++++++- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index 374f7050..cef933b4 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -188,7 +188,7 @@ router.post( "application/json": { schema: { type: "array", - items: { $ref: '#/components/schemas/Media' } + items: { $ref: '#/components/schemas/MediaListItem' } } } } diff --git a/apps/api/src/media/schemas.ts b/apps/api/src/media/schemas.ts index 31be3db5..bd91acbc 100644 --- a/apps/api/src/media/schemas.ts +++ b/apps/api/src/media/schemas.ts @@ -26,6 +26,17 @@ export interface MediaResponse { group?: string; } +export interface MediaListItemResponse { + mediaId: string; + originalFileName: string; + mimeType: string; + size: number; + access: AccessControl; + thumbnail: string; + caption?: string; + group?: string; +} + export const mediaResponseSchema = Joi.object({ mediaId: Joi.string().required(), originalFileName: Joi.string().required(), @@ -33,7 +44,18 @@ export const mediaResponseSchema = Joi.object({ size: Joi.number().required(), access: Joi.string().valid("public", "private").required(), file: Joi.string().uri().required(), - thumbnail: Joi.string().uri().optional(), + thumbnail: Joi.string().uri().optional().allow(""), + caption: Joi.string().optional().allow(""), + group: Joi.string().optional(), +}); + +export const mediaListItemResponseSchema = Joi.object({ + mediaId: Joi.string().required(), + originalFileName: Joi.string().required(), + mimeType: Joi.string().required(), + size: Joi.number().required(), + access: Joi.string().valid("public", "private").required(), + thumbnail: Joi.string().uri().optional().allow(""), caption: Joi.string().optional().allow(""), group: Joi.string().optional(), }); diff --git a/apps/api/src/swagger-generator.ts b/apps/api/src/swagger-generator.ts index e6e1692b..2a0ca685 100644 --- a/apps/api/src/swagger-generator.ts +++ b/apps/api/src/swagger-generator.ts @@ -7,6 +7,7 @@ import { uploadMediaSchema, getMediaSchema, mediaResponseSchema, + mediaListItemResponseSchema, mediaCountResponseSchema, mediaSizeResponseSchema, } from "./media/schemas"; @@ -107,6 +108,7 @@ swaggerAutogen()(outputFile, routes, doc).then(() => { content.components.schemas = { ...content.components.schemas, Media: joiToSwagger(mediaResponseSchema).swagger, + MediaListItem: joiToSwagger(mediaListItemResponseSchema).swagger, MediaSettings: joiToSwagger(mediaSettingsResponseSchema).swagger, MediaSettingsPayload: joiToSwagger(mediaSettingsSchema).swagger, UploadMediaPayload: joiToSwagger(uploadMediaSchema).swagger, @@ -168,6 +170,20 @@ swaggerAutogen()(outputFile, routes, doc).then(() => { }, }, }; + operation.responses = operation.responses || {}; + operation.responses["200"] = { + description: "List retrieved successfully", + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/MediaListItem", + }, + }, + }, + }, + }; } operation.responses = operation.responses || {}; diff --git a/apps/api/src/swagger_output.json b/apps/api/src/swagger_output.json index e731d1c0..22b0097e 100644 --- a/apps/api/src/swagger_output.json +++ b/apps/api/src/swagger_output.json @@ -597,7 +597,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Media" + "$ref": "#/components/schemas/MediaListItem" } } } @@ -869,6 +869,48 @@ ], "additionalProperties": false }, + "MediaListItem": { + "type": "object", + "properties": { + "mediaId": { + "type": "string" + }, + "originalFileName": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "number", + "format": "float" + }, + "access": { + "type": "string", + "enum": [ + "public", + "private" + ] + }, + "thumbnail": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "group": { + "type": "string" + } + }, + "required": [ + "mediaId", + "originalFileName", + "mimeType", + "size", + "access" + ], + "additionalProperties": false + }, "MediaSettings": { "type": "object", "properties": { From b2e74f6746916f6b4412bde48ebd1bbf38b975b0 Mon Sep 17 00:00:00 2001 From: Rajat Date: Fri, 20 Mar 2026 19:49:52 +0530 Subject: [PATCH 7/7] filtering by prefix in get media route --- apps/api/src/media/queries.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/api/src/media/queries.ts b/apps/api/src/media/queries.ts index 5624c735..e1ebe947 100644 --- a/apps/api/src/media/queries.ts +++ b/apps/api/src/media/queries.ts @@ -4,6 +4,10 @@ import GetPageProps from "./GetPageProps"; import MediaModel from "./model"; import { Constants, type MediaWithUserId } from "@medialit/models"; +function escapeRegex(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + export async function getMedia({ userId, apikey, @@ -89,8 +93,8 @@ export async function getPaginatedMedia({ ? Constants.AccessControl.PRIVATE : Constants.AccessControl.PUBLIC; } - if (group) { - query.group = group; + if (typeof group === "string" && group.trim().length > 0) { + query.group = { $regex: `^${escapeRegex(group.trim())}` }; } const limitWithFallback = recordsPerPage || numberOfRecordsPerPage;