Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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