From 4ab2b3a52169723971de66b10d2d3814c24c5871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20L=C3=A1szl=C3=B3?= <rlacko99@gmail.com> Date: Thu, 22 Oct 2020 18:57:59 +0200 Subject: [PATCH] upload card image --- package-lock.json | 118 +++++++++++++++++- package.json | 2 + src/index.ts | 5 + src/middlewares/files/cardImageStorage.ts | 13 ++ .../files/profilePictureStorage.ts | 13 ++ src/middlewares/files/uploadCardImage.ts | 28 +++++ src/middlewares/files/uploadProfilePicture.ts | 60 +++++++++ src/models/CardSchema.ts | 2 +- src/models/FileSchema.ts | 27 ++++ src/models/ProfileSchema.ts | 4 +- src/routes/file.ts | 27 ++++ src/utils/declarations/request.d.ts | 4 +- uploads/profile_picture/.gitkeep | 0 13 files changed, 296 insertions(+), 7 deletions(-) create mode 100644 src/middlewares/files/cardImageStorage.ts create mode 100644 src/middlewares/files/profilePictureStorage.ts create mode 100644 src/middlewares/files/uploadCardImage.ts create mode 100644 src/middlewares/files/uploadProfilePicture.ts create mode 100644 src/models/FileSchema.ts create mode 100644 src/routes/file.ts create mode 100644 uploads/profile_picture/.gitkeep diff --git a/package-lock.json b/package-lock.json index 7d734642..a0ce8cd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -953,6 +953,15 @@ "@types/node": "*" } }, + "@types/multer": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz", + "integrity": "sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/node": { "version": "13.7.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.1.tgz", @@ -1113,6 +1122,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1543,6 +1557,38 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1784,6 +1830,17 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "configstore": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", @@ -2056,6 +2113,38 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -5041,7 +5130,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -5049,8 +5137,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -5175,6 +5262,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -6741,6 +6843,11 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-length": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", @@ -7235,6 +7342,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", diff --git a/package.json b/package.json index 06c4f3e1..01390233 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/jest": "^25.1.3", "@types/mongoose": "^5.7.1", "@types/morgan": "^1.9.1", + "@types/multer": "^1.4.4", "@types/node": "^13.7.1", "jest": "^25.1.0", "morgan": "^1.10.0", @@ -35,6 +36,7 @@ "express": "^4.17.1", "express-session": "^1.17.0", "mongoose": "^5.9.1", + "multer": "^1.4.2", "simple-oauth2": "^3.3.0", "ts-node-dev": "^1.0.0" } diff --git a/src/index.ts b/src/index.ts index c9c1b746..95927e30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,9 +5,12 @@ import express, { Application, NextFunction, Request, Response } from "express"; import authRoute from "./routes/auth"; import bodyParser from "body-parser"; import expressSession from "express-session"; +import fileRoute from "./routes/file"; import mongoose from "mongoose"; import morgan from "morgan"; +import multer from "multer"; import newsRoute from "./routes/news"; +import path from "path"; import usersRoute from "./routes/user"; mongoose @@ -58,6 +61,8 @@ newsRoute(app); usersRoute(app); +fileRoute(app); + app.use((err: any, req: Request, res: Response, next: NextFunction) => { res.status(500).send("Houston, we have a problem!"); diff --git a/src/middlewares/files/cardImageStorage.ts b/src/middlewares/files/cardImageStorage.ts new file mode 100644 index 00000000..fa54de38 --- /dev/null +++ b/src/middlewares/files/cardImageStorage.ts @@ -0,0 +1,13 @@ +import multer from "multer"; +import path from "path"; + +export const cardImageStorage = multer.diskStorage({ + destination: function (req, file, callback) { + callback(null, "uploads/card_images/"); + }, + filename: function (req, file, callback) { + callback(null, Date.now() + "-" + file.originalname); + }, +}); + +export default cardImageStorage; diff --git a/src/middlewares/files/profilePictureStorage.ts b/src/middlewares/files/profilePictureStorage.ts new file mode 100644 index 00000000..5f484bcc --- /dev/null +++ b/src/middlewares/files/profilePictureStorage.ts @@ -0,0 +1,13 @@ +import multer from "multer"; +import path from "path"; + +export const profilePictureStorage = multer.diskStorage({ + destination: function (req, file, callback) { + callback(null, "uploads/profile_picture/"); + }, + filename: function (req, file, callback) { + callback(null, Date.now() + "-" + file.originalname); + }, +}); + +export default profilePictureStorage; diff --git a/src/middlewares/files/uploadCardImage.ts b/src/middlewares/files/uploadCardImage.ts new file mode 100644 index 00000000..2cb64f97 --- /dev/null +++ b/src/middlewares/files/uploadCardImage.ts @@ -0,0 +1,28 @@ +import File, { FileType } from "../../models/FileSchema"; +import { NextFunction, Request, Response } from "express"; + +const uploadCardImage = () => async ( + req: Request, + res: Response, + next: NextFunction +) => { + const uploadedFile = req.file; + if (!uploadedFile) { + return res.status(400).send("File wasn't provided!"); + } + const file = new File(); + + file.originalName = uploadedFile.originalname; + file.mimetype = uploadedFile.mimetype; + file.path = uploadedFile.path; + file.type = FileType.CARD_IMAGE; + + await file.save((err) => { + if (err) { + return res.status(400); + } + }); + return res.status(201).send(true); +}; + +export default uploadCardImage; diff --git a/src/middlewares/files/uploadProfilePicture.ts b/src/middlewares/files/uploadProfilePicture.ts new file mode 100644 index 00000000..0ffaf836 --- /dev/null +++ b/src/middlewares/files/uploadProfilePicture.ts @@ -0,0 +1,60 @@ +import File, { FileType } from "../../models/FileSchema"; +import { NextFunction, Request, Response } from "express"; + +import Profile from "../../models/ProfileSchema"; +import fs from "fs"; + +const uploadProfilePicture = () => async ( + req: Request, + res: Response, + next: NextFunction +) => { + const uploadedFile = req.file; + if (!uploadedFile) { + return res.status(400).send("File wasn't provided!"); + } + + Profile.findOne( + { external_id: req.session!.user!.id }, + async (error, profile) => { + if (error) { + console.warn(error); + res.status(400); + } else { + if (profile!.pictureId) { + const oldFile = await File.findById(profile!.pictureId).exec(); + + fs.unlink(oldFile!.path, (err) => { + if (err) { + console.error(err); + } + }); + + await File.deleteOne({ _id: profile!.pictureId }).exec(); + } + + const newFile = new File(); + + newFile.originalName = uploadedFile.originalname; + newFile.mimetype = uploadedFile.mimetype; + newFile.path = uploadedFile.path; + newFile.type = FileType.CARD_IMAGE; + + await newFile.save((err) => { + if (err) { + return res.status(400); + } + }); + + await Profile.updateOne( + { external_id: req.session!.user!.id }, + { pictureId: newFile.id } + ).exec(); + + return res.status(200).send(true); + } + } + ); +}; + +export default uploadProfilePicture; diff --git a/src/models/CardSchema.ts b/src/models/CardSchema.ts index d3b340bc..5a3c3ab3 100644 --- a/src/models/CardSchema.ts +++ b/src/models/CardSchema.ts @@ -13,7 +13,7 @@ export interface ICard extends Document { const CardSchema = new Schema({ // _id: card Number user: { type: ProfileSchema, required: true }, - backgroundImage: { type: String }, + backgroundImage: { type: Schema.Types.ObjectId, ref: "File" }, createDate: { type: Date, required: true }, expirationDate: { type: Date, required: true }, isTaken: { type: Boolean, required: true, default: false }, diff --git a/src/models/FileSchema.ts b/src/models/FileSchema.ts new file mode 100644 index 00000000..8405b403 --- /dev/null +++ b/src/models/FileSchema.ts @@ -0,0 +1,27 @@ +import { Document, Schema, model } from "mongoose"; + +export enum FileType { + CARD_IMAGE, + PROFILE_PICTURE, +} + +export interface IFile extends Document { + path: string; + originalName: string; + encoding: string; + mimetype: string; + type: FileType; +} + +const FileSchema = new Schema({ + path: { type: String, required: true }, + originalName: { type: String, required: true }, + mimetype: { type: String, required: true }, + type: { + type: String, + enum: Object.keys(FileType).map((k) => FileType[k as any]), + required: true, + }, +}); + +export default model<IFile>("File", FileSchema); diff --git a/src/models/ProfileSchema.ts b/src/models/ProfileSchema.ts index 0d5ffd64..b784b47b 100644 --- a/src/models/ProfileSchema.ts +++ b/src/models/ProfileSchema.ts @@ -13,7 +13,7 @@ export interface IProfile extends Document { external_id: string; studentCardNumber: string; roomNumber?: string; - picture: string; + pictureId: string; role: Role; email?: string; name?: string; @@ -24,7 +24,7 @@ const ProfileSchema = new Schema({ external_id: { type: String, required: true, unique: true, dropDups: true }, studentCardNumber: { type: String, required: false }, roomNumber: { type: String }, - picture: { type: String }, + pictureId: { type: Schema.Types.ObjectId, ref: "File", required: false }, role: { type: String, enum: Object.keys(Role).map((k) => Role[k as any]), diff --git a/src/routes/file.ts b/src/routes/file.ts new file mode 100644 index 00000000..a6c35c93 --- /dev/null +++ b/src/routes/file.ts @@ -0,0 +1,27 @@ +import { Application } from "express"; +import authenticated from "../middlewares/auth/authenticated"; +import cardImageStorage from "../middlewares/files/cardImageStorage"; +import multer from "multer"; +import profilePictureStorage from "../middlewares/files/profilePictureStorage"; +import responseUser from "../middlewares/user/responseUser"; +import uploadCardImage from "../middlewares/files/uploadCardImage"; +import uploadProfilePicture from "../middlewares/files/uploadProfilePicture"; + +const cardImageUpload = multer({ storage: cardImageStorage }); +const profilePictureUpload = multer({ storage: profilePictureStorage }); + +const fileRoute = (app: Application): void => { + app.post( + "/api/v1/files/card", + cardImageUpload.single("card_image"), + uploadCardImage() + ); + app.post( + "/api/v1/files/profile", + authenticated(), + profilePictureUpload.single("profile_picture"), + uploadProfilePicture() + ); +}; + +export default fileRoute; diff --git a/src/utils/declarations/request.d.ts b/src/utils/declarations/request.d.ts index 5875527d..d5ead211 100644 --- a/src/utils/declarations/request.d.ts +++ b/src/utils/declarations/request.d.ts @@ -1,3 +1,5 @@ declare namespace Express { - export interface Request {} + export interface Request { + fileValidationError: string; + } } diff --git a/uploads/profile_picture/.gitkeep b/uploads/profile_picture/.gitkeep new file mode 100644 index 00000000..e69de29b -- GitLab