diff --git a/package.json b/package.json index d247c39..9d69e55 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@prisma/client": "^3.3.0", "@types/handlebars": "^4.1.0", + "@types/express-fileupload": "^1.2.1", "@types/jsonwebtoken": "^8.5.5", "@types/mjml": "^4.7.0", "@types/node": "^16.10.3", @@ -31,6 +32,9 @@ "express": "^4.17.1", "handlebars": "^4.7.7", "express-async-errors": "^3.1.1", + "express-fileupload": "^1.2.1", + "file-type": "^16.5.3", + "is-svg": "^4.3.2", "jsonwebtoken": "^8.5.1", "mjml": "^4.11.0", "nanoid": "^3.3.3", diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts new file mode 100644 index 0000000..97b246b --- /dev/null +++ b/src/Controllers/media.controller.ts @@ -0,0 +1,109 @@ +import { Request, Response } from "express"; +import fs from "fs"; +import isSvg from "is-svg"; +import { fromBuffer as fileTypeFromBuffer } from "file-type"; + +export const uploadImage = async (req: Request, res: Response) => { + if (!req.auth?.isAuthenticated) { + return res.status(500).json({ + type: "failure", + payload: { + message: "The server was not able to validate your credentials; Please try again later", + }, + }); + } + + if (!(req.auth.permission_level === "ELEVATED")) { + return res.status(403).json({ + type: "error", + payload: { + message: "You do not have sufficient permissions to use this feature", + required_level: "ELEVATED", + }, + }); + } + + if (!req.files || !("file" in req.files)) { + return res.json({ + type: "error", + payload: { + message: "The format of the data was not valid", + schema: { + multipart: { + file: "binary file", + }, + }, + }, + }); + } + + const file = req.files.file; + + if (Array.isArray(file)) { + return res.status(500).json({ + type: "failure", + payload: { + message: "Invalid application state", + }, + }); + } + + if (file.size > 1024 * 1024) { + // Maybe resize (express-fileupload seems to support this) + return res.status(413).json({ + type: "error", + payload: { + message: "The uploaded image is too large", + max_size: "1 MB", + }, + }); + } + + const fileIsSvg = isSvg(file.data); + const fileType = await fileTypeFromBuffer(file.data); // Could be optimized + + if (!(fileType && ["image/jpeg", "image/png"].includes(fileType.mime))) { + if (!fileIsSvg) + return res.status(415).json({ + type: "error", + payload: { + message: + "The uploaded image does not satisfy the MIME type constraints (Only image/jpeg, image/png and SVG files are accepted)", + }, + }); + } + + const fileName = file.md5 + (fileIsSvg ? ".svg" : "." + fileType?.ext); + + if (fs.existsSync("media/" + fileName)) { + return res.status(409).json({ + type: "error", + payload: { + message: "The uploaded file already exists", + }, + _links: [ + { + rel: "self", + type: "GET", + href: "/api/media/" + fileName, + }, + ], + }); + } + + file.mv("media/" + fileName, console.error); + + return res.status(201).json({ + type: "success", + payload: { + message: "The file was uploaded and created on the server", + }, + _links: [ + { + rel: "self", + type: "GET", + href: "/api/media/" + fileName, + }, + ], + }); +}; diff --git a/src/Routes/media.routes.ts b/src/Routes/media.routes.ts index d67c9cc..e58b490 100644 --- a/src/Routes/media.routes.ts +++ b/src/Routes/media.routes.ts @@ -1,7 +1,12 @@ import express from "express"; +import fileUpload from "express-fileupload"; +import { requireAuthentication } from "../Middleware/auth/auth"; +import { uploadImage } from "../Controllers/media.controller"; const router = express.Router(); +router.post("/", requireAuthentication, fileUpload(), uploadImage); + // Media storage with express static (Should only handle GET and HEAD request methods) router.use("/", express.static("media", { redirect: false })); diff --git a/src/app.ts b/src/app.ts index 5f36efe..95bbc8b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -52,7 +52,6 @@ async function main() { app.use("/api/admins", adminRouter); -<<<<<<< HEAD app.use("/api/organisations", organisationRouter); app.use("/api/groups", groupRouter); @@ -63,9 +62,11 @@ async function main() { // Error handling app.use(defaultErrorHandler); // Not working -======= + app.use("/api/media", mediaRouter); ->>>>>>> Add express.static() to serve semi-static files + + // Disable the media router for now + // app.use("/api/media", mediaRouter); app.listen(process.env.PORT, () => { logger.info(`Listening on port ${process.env.PORT}`);