Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Dev Environment Tips

- Use `pnpm` as the package manager.
- This is a monorepo, so use `pnpm --filter <package-name> <command>` to run commands in specific packages.
4 changes: 4 additions & 0 deletions apps/api/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion apps/api/__tests__/media/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {});
Expand Down
19 changes: 12 additions & 7 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
}
2 changes: 1 addition & 1 deletion apps/api/src/apikey/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
97 changes: 79 additions & 18 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;

Expand Down
10 changes: 2 additions & 8 deletions apps/api/src/media-settings/handlers.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
58 changes: 56 additions & 2 deletions apps/api/src/media-settings/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
17 changes: 17 additions & 0 deletions apps/api/src/media-settings/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Joi from "joi";

export interface MediaSettingsResponse {
useWebP: boolean;
webpOutputQuality: number;
thumbnailHeight: number;
thumbnailWidth: number;
}

export const mediaSettingsSchema = Joi.object<MediaSettingsResponse>({
useWebP: Joi.boolean(),
webpOutputQuality: Joi.number().min(0).max(100),
thumbnailWidth: Joi.number().positive(),
thumbnailHeight: Joi.number().positive(),
});

export const mediaSettingsResponseSchema = mediaSettingsSchema;
24 changes: 17 additions & 7 deletions apps/api/src/media-settings/service.ts
Original file line number Diff line number Diff line change
@@ -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<Omit<MediaSettings, "userId" | "apikey"> | null> {
): Promise<MediaSettingsResponse> {
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,
};
}

Expand Down
19 changes: 5 additions & 14 deletions apps/api/src/media/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 });

Expand Down
Loading
Loading