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..5be0e951 --- /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.3 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..b495544e 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,88 @@ 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, { + explorer: true, + swaggerOptions: { + persistAuthorization: true, + displayRequestDuration: true, + docExpansion: "none", + defaultModelsExpandDepth: -1, + validatorUrl: null, + }, + }), +); 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.ignore = true */ + async (req, res) => { + await cleanupExpiredTempUploads(); + res.status(200).json({ + message: "Expired temp uploads cleaned up", + }); + }, +); +app.get( + "/cleanup/tus", + /* #swagger.ignore = true */ + 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..7cc98070 100644 --- a/apps/api/src/media-settings/routes.ts +++ b/apps/api/src/media-settings/routes.ts @@ -8,9 +8,63 @@ 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[400] = { description: 'Bad Request' } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[500] = { description: 'Internal Server Error' } + */ + 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' } + #swagger.responses[500] = { description: 'Internal Server Error' } + */ + 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..e6fe61f9 100644 --- a/apps/api/src/media-settings/service.ts +++ b/apps/api/src/media-settings/service.ts @@ -1,21 +1,31 @@ -import { MediaSettings } from "./model"; 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 | null> { +): Promise { const mediaSettings = await queries.getMediaSettings(userId, apikey); if (!mediaSettings) { - return {}; + return { + useWebP: false, + webpOutputQuality: 0, + thumbnailHeight: defaultThumbnailHeight, + thumbnailWidth: defaultThumbnailWidth, + }; } return { - useWebP: mediaSettings.useWebP, - webpOutputQuality: mediaSettings.webpOutputQuality, - thumbnailHeight: mediaSettings.thumbnailHeight, - thumbnailWidth: mediaSettings.thumbnailWidth, + useWebP: mediaSettings.useWebP || false, + webpOutputQuality: mediaSettings.webpOutputQuality || 0, + thumbnailHeight: + mediaSettings.thumbnailHeight || defaultThumbnailHeight, + thumbnailWidth: mediaSettings.thumbnailWidth || defaultThumbnailWidth, }; } diff --git a/apps/api/src/media/handlers.ts b/apps/api/src/media/handlers.ts index 79659776..709b2289 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,14 +80,9 @@ 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 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/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; diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index c2d72ac7..cef933b4 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -21,9 +21,57 @@ 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. Use API key auth from Authorize (`x-medialit-apikey`) or pass `x-medialit-signature` for this endpoint only.' + #swagger.security = [{ "apiKeyAuth": [] }, { "signatureAuth": [] }] + #swagger.parameters['x-medialit-signature'] = { + in: 'header', + description: 'Upload Signature for secure client-side uploads', + schema: { type: 'string' }, + required: false + } + #swagger.requestBody = { + required: true, + 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" } + }, + required: ["file"] + } + } + } + } + #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' } + #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({ useTempFiles: true, @@ -48,11 +96,169 @@ 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' } + #swagger.responses[500] = { description: 'Internal Server Error' } + */ + apikey, + getMediaDetails, +); +router.post( + "/get", + /* + #swagger.tags = ['Media'] + #swagger.summary = 'List Media' + #swagger.description = 'List media files filtered by optional query payload.' + #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.requestBody = { + required: false, + content: { + "application/json": { + schema: { $ref: '#/components/schemas/GetMediaQuery' } + } + } + } + #swagger.responses[200] = { + description: 'List retrieved successfully', + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: '#/components/schemas/MediaListItem' } + } + } + } + } + #swagger.responses[400] = { description: 'Bad Request' } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[500] = { description: 'Internal Server Error' } + */ + 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' } + #swagger.responses[404] = { description: 'Media not found' } + #swagger.responses[500] = { description: 'Internal Server Error' } + */ + 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[500] = { description: 'Internal Server Error' } + */ + 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..bd91acbc --- /dev/null +++ b/apps/api/src/media/schemas.ts @@ -0,0 +1,70 @@ +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 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(), + mimeType: Joi.string().required(), + size: Joi.number().required(), + access: Joi.string().valid("public", "private").required(), + file: Joi.string().uri().required(), + 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(), +}); + +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/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 018c8115..1db200bd 100644 --- a/apps/api/src/signature/routes.ts +++ b/apps/api/src/signature/routes.ts @@ -3,6 +3,29 @@ 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[400] = { description: 'Bad Request' } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[500] = { description: 'Internal Server Error' } + */ + 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..2a0ca685 --- /dev/null +++ b/apps/api/src/swagger-generator.ts @@ -0,0 +1,221 @@ +import swaggerAutogen from "swagger-autogen"; +import joiToSwagger from "joi-to-swagger"; +import path from "path"; +import fs from "fs"; + +import { + uploadMediaSchema, + getMediaSchema, + mediaResponseSchema, + mediaListItemResponseSchema, + mediaCountResponseSchema, + mediaSizeResponseSchema, +} from "./media/schemas"; +import { + mediaSettingsSchema, + mediaSettingsResponseSchema, +} from "./media-settings/schemas"; +import { signatureResponseSchema } from "./signature/schemas"; + +const doc = { + 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: [ + { + url: "{protocol}://{host}", + description: "API Server", + variables: { + protocol: { + default: "https", + enum: ["https", "http"], + }, + host: { + default: "api.medialit.cloud", + }, + }, + }, + ], + tags: [ + { + name: "Media", + description: "Upload, list, and manage media resources.", + }, + { + name: "Settings", + description: "Manage media processing configuration.", + }, + ], + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + in: "header", + name: "x-medialit-apikey", + }, + signatureAuth: { + type: "apiKey", + in: "header", + name: "x-medialit-signature", + }, + }, + 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")); + + 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, + Media: joiToSwagger(mediaResponseSchema).swagger, + MediaListItem: joiToSwagger(mediaListItemResponseSchema).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, + 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 === "/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 || {}; + operation.responses["200"] = { + description: "List retrieved successfully", + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/MediaListItem", + }, + }, + }, + }, + }; + } + + 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; + } + + 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..22b0097e --- /dev/null +++ b/apps/api/src/swagger_output.json @@ -0,0 +1,1068 @@ +{ + "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": [ + { + "url": "{protocol}://{host}", + "description": "API Server", + "variables": { + "protocol": { + "default": "https", + "enum": [ + "https", + "http" + ] + }, + "host": { + "default": "api.medialit.cloud" + } + } + } + ], + "tags": [ + { + "name": "Media", + "description": "Upload, list, and manage media resources." + }, + { + "name": "Settings", + "description": "Manage media processing configuration." + } + ], + "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 + } + } + } + } + } + } + }, + "operationId": "getHealth", + "security": [] + } + }, + "/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" + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "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": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSettingsPayload" + } + } + } + }, + "operationId": "updateMediaSettings" + } + }, + "/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", + "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": "getMediaSettings" + } + }, + "/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" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "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": { + "post": { + "tags": [ + "Media" + ], + "summary": "Upload Media", + "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", + "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", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "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": [ + { + "apiKeyAuth": [] + }, + { + "signatureAuth": [] + } + ], + "requestBody": { + "required": true, + "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" + } + }, + "required": [ + "file" + ] + } + } + } + }, + "operationId": "uploadMedia" + } + }, + "/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", + "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": "getMediaCount" + } + }, + "/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", + "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": "getMediaSize" + } + }, + "/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, + "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", + "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": "getMediaDetails" + } + }, + "/media/get": { + "post": { + "tags": [ + "Media" + ], + "summary": "List Media", + "description": "List media files filtered by optional query payload.", + "responses": { + "200": { + "description": "List retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaListItem" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "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}": { + "post": { + "tags": [ + "Media" + ], + "summary": "Seal Media", + "description": "Seal a media file (mark as processed/finalized).", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "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", + "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}": { + "delete": { + "tags": [ + "Media" + ], + "summary": "Delete Media", + "description": "Permanently delete a media file.", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "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", + "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": "deleteMedia" + } + } + }, + "components": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "x-medialit-apikey" + }, + "signatureAuth": { + "type": "apiKey", + "in": "header", + "name": "x-medialit-signature" + } + }, + "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" + ], + "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": { + "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 + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + }, + "required": [ + "error" + ], + "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/apps/docs/content/docs/rest-api.mdx b/apps/docs/content/docs/rest-api.mdx index 4979b90d..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 -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.3 and is available [here](https://api.medialit.cloud/docs). ## Video Tutorial - \ 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: {}