diff --git a/.gitignore b/.gitignore index e4e5ae6d7f354cafe33fd718c282e738d8a49b31..115e10fd00b924aa8a3c09305afe7b64f58fca24 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,6 @@ dist # config files config.json + +# file where we store pdf +files/* diff --git a/package-lock.json b/package-lock.json index 281e305b46292835dd3dd5c94dce59946bb73a23..401ac877b7e04bd6d678617cb537babf644273c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "eslint-import-resolver-typescript": "^3.5.2", "fastify": "^4.9.2", "mongoose": "^6.7.2", + "multer": "^1.4.5-lts.1", "passport": "^0.6.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", @@ -37,6 +38,7 @@ "@types/passport-jwt": "^3.0.8", "@types/passport-local": "^1.0.34", "@types/supertest": "^2.0.11", + "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "cross-env": "^7.0.3", @@ -2683,6 +2685,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", @@ -3232,6 +3251,12 @@ "@types/superagent": "*" } }, + "node_modules/@types/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", + "dev": true + }, "node_modules/@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -8255,9 +8280,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", @@ -12979,6 +13004,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": { @@ -13468,6 +13509,12 @@ "@types/superagent": "*" } }, + "@types/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", + "dev": true + }, "@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -17223,9 +17270,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 ce7cb37cf1166ee2c63ff9e6c7f8888476489125..59ed794c5e0baad88e3a45216060b8d0673b9a10 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "eslint-import-resolver-typescript": "^3.5.2", "fastify": "^4.9.2", "mongoose": "^6.7.2", + "multer": "^1.4.5-lts.1", "passport": "^0.6.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", @@ -53,6 +54,7 @@ "@types/passport-jwt": "^3.0.8", "@types/passport-local": "^1.0.34", "@types/supertest": "^2.0.11", + "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "cross-env": "^7.0.3", diff --git a/src/app.module.ts b/src/app.module.ts index b0ed19151deea8f71a8695e2c3ae05f300f3642f..2c246c578f6188c5bfe8d3ad493285dab30f8d95 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,12 +5,14 @@ import { PeopleModule } from './people/people.module'; import { GroupsModule } from './groups/groups.module'; import { LoginModule } from './login/login.module'; import { InternshipsModule } from './internships/internships.module'; +import { ResourcesModule } from './resources/resources.module'; @Module({ imports: [ PeopleModule, GroupsModule, InternshipsModule, + ResourcesModule, MongooseModule.forRoot(config.mongodb.uri), LoginModule ], diff --git a/src/config/config.model.ts b/src/config/config.model.ts index 79984c4772b4ffd39b85c626dd4b79773b210239..b89c81748bf1996838ee041549c23b0c9ef654e9 100644 --- a/src/config/config.model.ts +++ b/src/config/config.model.ts @@ -1,12 +1,19 @@ -export interface IServerConfig { +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 221da47e94f8ee7466bfef5535006d094832ff04..68a5550e9693089ed1d786acbcdeb74187f39d96 100644 --- a/src/config/config.prod.json +++ b/src/config/config.prod.json @@ -3,6 +3,9 @@ "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 ee6bb8331d52b839ede08f6ab08f39349591a7f5..68a5550e9693089ed1d786acbcdeb74187f39d96 100644 --- a/src/config/config.template.json +++ b/src/config/config.template.json @@ -1,7 +1,11 @@ { "server": { + "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 e948aaeb8cdb7af0e37d8b5ec06c5e704881c2c0..70281753480b8a73a4f2d8404d9f1d42c846f961 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -7,7 +7,8 @@ import { exit } from 'process'; const CONFIG_DEV = 'config.json'; const CONFIG_PROD = 'config.prod.json'; -let config; +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 b5bfdfc5babc8139f359b4db96dd05c6e8545377..22a08178f9e288cc68691af8cf6e3a570ea36dfa 100644 --- a/src/internships/dao/internships.dao.ts +++ b/src/internships/dao/internships.dao.ts @@ -120,8 +120,9 @@ 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(typeof content); + console.log(content); let nextState: string, contentHolder: string; let valid = false; switch (state) { diff --git a/src/internships/internships.controller.ts b/src/internships/internships.controller.ts index 1fb011eb7d7a98fa8e58cf5981bf003c48d7033a..3db74f04f2bddd9213935ca4e5cde9c0590260e7 100644 --- a/src/internships/internships.controller.ts +++ b/src/internships/internships.controller.ts @@ -7,13 +7,23 @@ import { Param, Body, UseInterceptors, + UploadedFile, } from '@nestjs/common'; -import { BAD_TRACKING_STATE } from 'src/shared/HttpError'; +import { BAD_REQUEST, BAD_TRACKING_STATE } from 'src/shared/HttpError'; import * as InternshipStates from 'src/shared/InternshipState'; import { HttpInterceptor } from '../interceptors/http.interceptor'; 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 { + STATE_RESPONSIBLE_ACCEPTS_INTERNSHIP_INFORMATION, + STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION, +} from 'src/shared/InternshipState'; +import config from 'src/config'; +import { Optional } from '@nestjs/common/decorators'; +import { v4 } from 'uuid'; +import { diskStorage } from 'multer'; @Controller('internships') @UseInterceptors(HttpInterceptor) @@ -47,18 +57,43 @@ export class InternshipsController { return this._internshipsService.update(params.studentId, internshipDto); } - @Put(':studentId/tracking') + // uploads even if invalid state... + @Put(':studentId/:state') + @UseInterceptors( + FileInterceptor('pdf', { + storage: diskStorage({ + destination: './files', + filename: (_req, _file, cb) => { + return cb(null, `${v4()}.pdf`); + }, + }), + }), + ) updateState( - @Param() params: { studentId: string }, - @Body() body: { state: string; content?: string | boolean }, + @Param() params: { studentId: string; state: string }, + @Optional() @Body() body: { content?: boolean }, + @Optional() @UploadedFile() file, ): Promise<InternshipEntity | void> { - if (!InternshipStates.isStateValid(body.state)) - throw BAD_TRACKING_STATE(body.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 + if (!InternshipStates.isStateValid(params.state)) + throw BAD_TRACKING_STATE(params.state); + if ( + params.state === STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION || + params.state === STATE_RESPONSIBLE_ACCEPTS_INTERNSHIP_INFORMATION + ) { + if (!body) throw BAD_REQUEST; + return this._internshipsService.updateTracking( + params.studentId, + params.state, + body.content, + ); + } + + if (!file) throw BAD_REQUEST; + console.log(params.state); return this._internshipsService.updateTracking( params.studentId, - body.state, - body.content, + params.state, + `${config.server.uri}:${config.server.port}/resources/agreements/${file.filename}`, ); } diff --git a/src/main.ts b/src/main.ts index c5c630c00749273794025342b1f60b6be049f12f..bffb6eaf43e8ba958fa978ecfa619bb646febfac 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 0000000000000000000000000000000000000000..afd5df97a65ed6741e182a9a5984392acaa1bf95 --- /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 0000000000000000000000000000000000000000..6cadff34ed1d90667bb42bdb003a39f3d876748b --- /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 aa1d3565c01e62554e91ff4537c67ecc952ff961..200448b6f55e01865f8f8350e4fdb9dfcfcfc028 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) =>