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