From 5eab22bc7d2712db895115810f6f986aacd5de1a Mon Sep 17 00:00:00 2001
From: Ivan Alglave <ivanalglave@outlook.fr>
Date: Tue, 13 Dec 2022 22:47:31 +0100
Subject: [PATCH] fix: uploading and downloading pdf now works

---
 .gitignore                                |  2 +-
 package-lock.json                         | 46 ++++++++++++++++++++---
 package.json                              |  1 +
 src/app.module.ts                         |  2 +
 src/config/config.model.ts                | 12 ++++--
 src/config/config.prod.json               |  5 ++-
 src/config/config.template.json           |  5 ++-
 src/config/index.ts                       |  3 ++
 src/internships/dao/internships.dao.ts    |  1 +
 src/internships/internships.controller.ts | 30 +++++----------
 src/main.ts                               |  2 +-
 src/resources/resources.controller.ts     | 31 +++++++++++++++
 src/resources/resources.module.ts         |  8 ++++
 src/shared/HttpError.ts                   |  1 +
 14 files changed, 116 insertions(+), 33 deletions(-)
 create mode 100644 src/resources/resources.controller.ts
 create mode 100644 src/resources/resources.module.ts

diff --git a/.gitignore b/.gitignore
index d1486f9..115e10f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -107,4 +107,4 @@ dist
 config.json
 
 # file where we store pdf
-internship-agreements/
\ No newline at end of file
+files/*
diff --git a/package-lock.json b/package-lock.json
index c34df50..b564a19 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
         "class-validator": "^0.13.2",
         "fastify": "^4.9.2",
         "mongoose": "^6.7.2",
+        "multer": "^1.4.5-lts.1",
         "reflect-metadata": "^0.1.13",
         "rimraf": "^3.0.2",
         "rxjs": "^7.2.0"
@@ -2661,6 +2662,23 @@
         "@nestjs/core": "^9.0.0"
       }
     },
+    "node_modules/@nestjs/platform-express/node_modules/multer": {
+      "version": "1.4.4-lts.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
+      "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+      "dependencies": {
+        "append-field": "^1.0.0",
+        "busboy": "^1.0.0",
+        "concat-stream": "^1.5.2",
+        "mkdirp": "^0.5.4",
+        "object-assign": "^4.1.1",
+        "type-is": "^1.6.4",
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
     "node_modules/@nestjs/schematics": {
       "version": "9.0.3",
       "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-9.0.3.tgz",
@@ -7518,9 +7536,9 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "node_modules/multer": {
-      "version": "1.4.4-lts.1",
-      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
-      "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+      "version": "1.4.5-lts.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
+      "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
       "dependencies": {
         "append-field": "^1.0.0",
         "busboy": "^1.0.0",
@@ -12039,6 +12057,22 @@
         "express": "4.18.2",
         "multer": "1.4.4-lts.1",
         "tslib": "2.4.1"
+      },
+      "dependencies": {
+        "multer": {
+          "version": "1.4.4-lts.1",
+          "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
+          "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+          "requires": {
+            "append-field": "^1.0.0",
+            "busboy": "^1.0.0",
+            "concat-stream": "^1.5.2",
+            "mkdirp": "^0.5.4",
+            "object-assign": "^4.1.1",
+            "type-is": "^1.6.4",
+            "xtend": "^4.0.0"
+          }
+        }
       }
     },
     "@nestjs/schematics": {
@@ -15754,9 +15788,9 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "multer": {
-      "version": "1.4.4-lts.1",
-      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
-      "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+      "version": "1.4.5-lts.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
+      "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
       "requires": {
         "append-field": "^1.0.0",
         "busboy": "^1.0.0",
diff --git a/package.json b/package.json
index 07f72a8..30ef151 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "class-validator": "^0.13.2",
     "fastify": "^4.9.2",
     "mongoose": "^6.7.2",
+    "multer": "^1.4.5-lts.1",
     "reflect-metadata": "^0.1.13",
     "rimraf": "^3.0.2",
     "rxjs": "^7.2.0"
diff --git a/src/app.module.ts b/src/app.module.ts
index e794431..9caaddf 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -4,12 +4,14 @@ import config from './config';
 import { PeopleModule } from './people/people.module';
 import { GroupsModule } from './groups/groups.module';
 import { InternshipsModule } from './internships/internships.module';
+import { ResourcesModule } from './resources/resources.module';
 
 @Module({
   imports: [
     PeopleModule,
     GroupsModule,
     InternshipsModule,
+    ResourcesModule,
     MongooseModule.forRoot(config.mongodb.uri),
   ],
 })
diff --git a/src/config/config.model.ts b/src/config/config.model.ts
index 991a78d..b89c817 100644
--- a/src/config/config.model.ts
+++ b/src/config/config.model.ts
@@ -1,13 +1,19 @@
-export interface IServerConfig {
-  url: string;
+interface IServerConfig {
+  uri: string;
   port: number;
 }
 
-export interface IMongodbConfig {
+interface IResources {
+  root: string;
+  agreements: string;
+}
+
+interface IMongodbConfig {
   uri: string;
 }
 
 export interface IConfig {
   server: IServerConfig;
+  resources: IResources;
   mongodb: IMongodbConfig;
 }
diff --git a/src/config/config.prod.json b/src/config/config.prod.json
index 9a88b14..68a5550 100644
--- a/src/config/config.prod.json
+++ b/src/config/config.prod.json
@@ -1,8 +1,11 @@
 {
   "server": {
-    "url": "localhost",
+    "uri": "localhost",
     "port": 3001
   },
+  "resources": {
+    "internshipAgreements": "internship-agreements"
+  },
   "mongodb": {
     "uri": "mongodb://localhost:27017/internship-manager"
   }
diff --git a/src/config/config.template.json b/src/config/config.template.json
index 9a88b14..68a5550 100644
--- a/src/config/config.template.json
+++ b/src/config/config.template.json
@@ -1,8 +1,11 @@
 {
   "server": {
-    "url": "localhost",
+    "uri": "localhost",
     "port": 3001
   },
+  "resources": {
+    "internshipAgreements": "internship-agreements"
+  },
   "mongodb": {
     "uri": "mongodb://localhost:27017/internship-manager"
   }
diff --git a/src/config/index.ts b/src/config/index.ts
index 2b462df..7028175 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -8,6 +8,7 @@ const CONFIG_DEV = 'config.json';
 const CONFIG_PROD = 'config.prod.json';
 
 let config: IConfig;
+console.log(process.env.NODE_ENV);
 // Load config based on env
 switch (process.env.NODE_ENV) {
   case 'dev':
@@ -30,5 +31,7 @@ switch (process.env.NODE_ENV) {
     exit(-1);
 }
 
+console.log(config);
+
 // Export config
 export default config;
diff --git a/src/internships/dao/internships.dao.ts b/src/internships/dao/internships.dao.ts
index 6429599..22a0817 100644
--- a/src/internships/dao/internships.dao.ts
+++ b/src/internships/dao/internships.dao.ts
@@ -120,6 +120,7 @@ export class InternshipDao {
     content: string | boolean,
   ): Promise<Internship | void> =>
     new Promise((resolve, reject) => {
+      console.log('%s/%s: {%s}', studentId, state, content);
       if (!isStateValid(state)) reject(BAD_REQUEST);
       console.log(content);
       let nextState: string, contentHolder: string;
diff --git a/src/internships/internships.controller.ts b/src/internships/internships.controller.ts
index 9b5cdb2..3db74f0 100644
--- a/src/internships/internships.controller.ts
+++ b/src/internships/internships.controller.ts
@@ -16,7 +16,6 @@ import { CreateInternshipDto } from './dto/create-internship.dto';
 import { InternshipEntity } from './entities/internship.entity';
 import { InternshipService } from './internships.service';
 import { FileInterceptor } from '@nestjs/platform-express';
-import * as path from 'path';
 import {
   STATE_RESPONSIBLE_ACCEPTS_INTERNSHIP_INFORMATION,
   STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION,
@@ -24,6 +23,7 @@ import {
 import config from 'src/config';
 import { Optional } from '@nestjs/common/decorators';
 import { v4 } from 'uuid';
+import { diskStorage } from 'multer';
 
 @Controller('internships')
 @UseInterceptors(HttpInterceptor)
@@ -57,14 +57,16 @@ export class InternshipsController {
     return this._internshipsService.update(params.studentId, internshipDto);
   }
 
+  // uploads even if invalid state...
   @Put(':studentId/:state')
   @UseInterceptors(
     FileInterceptor('pdf', {
-      dest: './internship-agreements',
-      fileFilter: (req, file, cb) => {
-        file.filename = `${v4()}.pdf`;
-        cb(null, true);
-      },
+      storage: diskStorage({
+        destination: './files',
+        filename: (_req, _file, cb) => {
+          return cb(null, `${v4()}.pdf`);
+        },
+      }),
     }),
   )
   updateState(
@@ -74,12 +76,6 @@ export class InternshipsController {
   ): Promise<InternshipEntity | void> {
     if (!InternshipStates.isStateValid(params.state))
       throw BAD_TRACKING_STATE(params.state);
-
-    // AMINE : Handle PDF file upload -> save file in /pdf/ folder and set content as local file URL. In case of step with no file, set content as true/false
-
-    //case where there isn't a file
-    //Deux premiers états
-
     if (
       params.state === STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION ||
       params.state === STATE_RESPONSIBLE_ACCEPTS_INTERNSHIP_INFORMATION
@@ -92,18 +88,12 @@ export class InternshipsController {
       );
     }
 
-    //case where there is a file
     if (!file) throw BAD_REQUEST;
-    console.log(file);
-    console.log(config);
+    console.log(params.state);
     return this._internshipsService.updateTracking(
       params.studentId,
       params.state,
-      path.join(
-        `${config.server.url}:${config.server.port}`,
-        file.path,
-        file.fieldname,
-      ),
+      `${config.server.uri}:${config.server.port}/resources/agreements/${file.filename}`,
     );
   }
 
diff --git a/src/main.ts b/src/main.ts
index c5c630c..bffb6ea 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,7 @@
+import config from './config';
 import { ValidationPipe } from '@nestjs/common';
 import { NestFactory } from '@nestjs/core';
 import { AppModule } from './app.module';
-import config from './config';
 
 async function bootstrap() {
   const env = process.env.NODE_ENV;
diff --git a/src/resources/resources.controller.ts b/src/resources/resources.controller.ts
new file mode 100644
index 0000000..afd5df9
--- /dev/null
+++ b/src/resources/resources.controller.ts
@@ -0,0 +1,31 @@
+import {
+  Controller,
+  Get,
+  Param,
+  UseInterceptors,
+  Res,
+  StreamableFile,
+} from '@nestjs/common';
+import { NOT_FOUND } from 'src/shared/HttpError';
+import { HttpInterceptor } from '../interceptors/http.interceptor';
+import { Response } from 'express';
+import { createReadStream, existsSync } from 'fs';
+
+@Controller('resources')
+@UseInterceptors(HttpInterceptor)
+export class ResourcesController {
+  @Get('agreements/:filename')
+  serveAgreement(
+    @Param('filename') filename,
+    @Res({ passthrough: true }) res: Response,
+  ): StreamableFile {
+    const filepath = `files\\${filename}`;
+    if (!existsSync(filepath)) throw NOT_FOUND;
+    const file = createReadStream(filepath);
+    res.set({
+      'Content-Type': 'application/pdf',
+      'Content-Disposition': 'attachment; filename="agreement.pdf"',
+    });
+    return new StreamableFile(file);
+  }
+}
diff --git a/src/resources/resources.module.ts b/src/resources/resources.module.ts
new file mode 100644
index 0000000..6cadff3
--- /dev/null
+++ b/src/resources/resources.module.ts
@@ -0,0 +1,8 @@
+import { Module, Logger } from '@nestjs/common';
+import { ResourcesController } from './resources.controller';
+
+@Module({
+  controllers: [ResourcesController],
+  providers: [Logger],
+})
+export class ResourcesModule {}
diff --git a/src/shared/HttpError.ts b/src/shared/HttpError.ts
index aa1d356..200448b 100644
--- a/src/shared/HttpError.ts
+++ b/src/shared/HttpError.ts
@@ -11,6 +11,7 @@ export const NOT_FOUND = new NotFoundException();
 export const CONFLICT = new ConflictException();
 export const BAD_REQUEST = new BadRequestException();
 export const INTERNAL = new InternalServerErrorException();
+export const UNPROCESSABLE_ENTITY = new UnprocessableEntityException();
 export const BAD_TRACKING_STATE = (badState: string) =>
   new UnprocessableEntityException(`Unknown state [${badState}]`);
 export const CUSTOM = (reason: string, errorStatus: number) =>
-- 
GitLab