From 95ed7eca7c099e24aeeb6ffb535e5b59858162fb Mon Sep 17 00:00:00 2001 From: Hugo SUBTIL <ext.sopra.husubtil@grandlyon.com> Date: Tue, 12 Jan 2021 12:26:13 +0100 Subject: [PATCH] feat: add admin module + add validation for claiming structures --- src/admin/admin.controller.spec.ts | 18 +++++++ src/admin/admin.controller.ts | 45 ++++++++++++++++ src/admin/admin.module.ts | 12 +++++ src/admin/admin.service.spec.ts | 18 +++++++ src/admin/admin.service.ts | 4 ++ src/admin/dto/pending-structure.dto.ts | 14 +++++ src/app.module.ts | 2 + src/configuration/config.ts | 4 ++ .../mail-templates/adminStructureClaim.ejs | 6 +++ .../mail-templates/adminStructureClaim.json | 3 ++ src/structures/structures.controller.ts | 1 + src/structures/structures.service.ts | 1 + src/users/users.controller.ts | 9 ---- src/users/users.service.ts | 53 +++++++++++++++++++ 14 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 src/admin/admin.controller.spec.ts create mode 100644 src/admin/admin.controller.ts create mode 100644 src/admin/admin.module.ts create mode 100644 src/admin/admin.service.spec.ts create mode 100644 src/admin/admin.service.ts create mode 100644 src/admin/dto/pending-structure.dto.ts create mode 100644 src/mailer/mail-templates/adminStructureClaim.ejs create mode 100644 src/mailer/mail-templates/adminStructureClaim.json diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts new file mode 100644 index 000000000..0b8ca9028 --- /dev/null +++ b/src/admin/admin.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminController } from './admin.controller'; + +describe('AdminController', () => { + let controller: AdminController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AdminController], + }).compile(); + + controller = module.get<AdminController>(AdminController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts new file mode 100644 index 000000000..db2e4620c --- /dev/null +++ b/src/admin/admin.controller.ts @@ -0,0 +1,45 @@ +import { Body } from '@nestjs/common'; +import { Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { ApiOperation } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { Roles } from '../users/decorators/roles.decorator'; +import { RolesGuard } from '../users/guards/roles.guard'; +import { UsersService } from '../users/users.service'; +import { PendingStructureDto } from './dto/pending-structure.dto'; + +@Controller('admin') +export class AdminController { + constructor(private usersService: UsersService) {} + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @Get('pendingStructures') + @ApiOperation({ description: 'Get pending structre for validation' }) + public getPendingAttachments() { + return this.usersService.getPendingStructures(); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @Post('validatePendingStructure') + @ApiOperation({ description: 'Validate structure ownership' }) + public validatePendingStructure(@Body() pendingStructureDto: PendingStructureDto) { + return this.usersService.validatePendingStructure( + pendingStructureDto.userEmail, + pendingStructureDto.structureId, + true + ); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @Post('rejectPendingStructure') + @ApiOperation({ description: 'Refuse structure ownership' }) + public refusePendingStructure(@Body() pendingStructureDto: PendingStructureDto) { + return this.usersService.validatePendingStructure( + pendingStructureDto.userEmail, + pendingStructureDto.structureId, + false + ); + } +} diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts new file mode 100644 index 000000000..3376e00f6 --- /dev/null +++ b/src/admin/admin.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UsersModule } from '../users/users.module'; +import { UsersService } from '../users/users.service'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; + +@Module({ + imports: [UsersModule], + controllers: [AdminController], + providers: [AdminService], +}) +export class AdminModule {} diff --git a/src/admin/admin.service.spec.ts b/src/admin/admin.service.spec.ts new file mode 100644 index 000000000..5e5e153df --- /dev/null +++ b/src/admin/admin.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminService } from './admin.service'; + +describe('AdminService', () => { + let service: AdminService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminService], + }).compile(); + + service = module.get<AdminService>(AdminService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts new file mode 100644 index 000000000..796f9fd1a --- /dev/null +++ b/src/admin/admin.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AdminService {} diff --git a/src/admin/dto/pending-structure.dto.ts b/src/admin/dto/pending-structure.dto.ts new file mode 100644 index 000000000..04936833a --- /dev/null +++ b/src/admin/dto/pending-structure.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsNumber } from 'class-validator'; + +export class PendingStructureDto { + @IsNotEmpty() + @IsEmail() + @ApiProperty({ type: String }) + readonly userEmail: string; + + @IsNotEmpty() + @IsNumber() + @ApiProperty({ type: Number }) + readonly structureId: number; +} diff --git a/src/app.module.ts b/src/app.module.ts index d3a7ebdbb..0d49be796 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; import { MailerModule } from './mailer/mailer.module'; import { TclModule } from './tcl/tcl.module'; +import { AdminModule } from './admin/admin.module'; @Module({ imports: [ ConfigurationModule, @@ -20,6 +21,7 @@ import { TclModule } from './tcl/tcl.module'; UsersModule, MailerModule, TclModule, + AdminModule, ], controllers: [AppController], }) diff --git a/src/configuration/config.ts b/src/configuration/config.ts index b6d29acca..34c5a4a24 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -21,5 +21,9 @@ export const config = { ejs: 'resetPassword.ejs', json: 'resetPassword.json', }, + adminStructureClaim: { + ejs: 'adminStructureClaim.ejs', + json: 'adminStructureClaim.json', + }, }, }; diff --git a/src/mailer/mail-templates/adminStructureClaim.ejs b/src/mailer/mail-templates/adminStructureClaim.ejs new file mode 100644 index 000000000..12ba53983 --- /dev/null +++ b/src/mailer/mail-templates/adminStructureClaim.ejs @@ -0,0 +1,6 @@ +Bonjour<br /> +<br /> +Une nouvelle structure a été revendiquée. Pour valider ou refuser la demande, merci de vous rendre sur +<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/admin">ce lien</a>. +<br /> +Ce mail est un mail automatique. Merci de ne pas y répondre. diff --git a/src/mailer/mail-templates/adminStructureClaim.json b/src/mailer/mail-templates/adminStructureClaim.json new file mode 100644 index 000000000..bad53f08f --- /dev/null +++ b/src/mailer/mail-templates/adminStructureClaim.json @@ -0,0 +1,3 @@ +{ + "subject": "Nouvelle demande de revendication de structure" +} diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 4c6ad99bb..e9b08fe97 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -22,6 +22,7 @@ export class StructuresController { } @Put(':id') + //TODO: protect, only structure owner can edit it public async update(@Param('id') id: number, @Body() body: structureDto) { return this.structureService.update(id, body); } diff --git a/src/structures/structures.service.ts b/src/structures/structures.service.ts index 85d241a5f..d1b9a7e53 100644 --- a/src/structures/structures.service.ts +++ b/src/structures/structures.service.ts @@ -26,6 +26,7 @@ export class StructuresService { createdStructure.save(); user.structuresLink.push(createdStructure.id); user.save(); + return createdStructure; } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index d78059dc5..9c8c2c31b 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -7,8 +7,6 @@ import { CreateUserDto } from './dto/create-user.dto'; import { PasswordResetApplyDto } from './dto/reset-password-apply.dto'; import { PasswordResetDto } from './dto/reset-password.dto'; import { UsersService } from './users.service'; -import { RolesGuard } from './guards/roles.guard'; -import { Roles } from './decorators/roles.decorator'; @Controller('users') export class UsersController { @@ -87,11 +85,4 @@ export class UsersController { public async resetPasswordApply(@Body() passwordResetApplyDto: PasswordResetApplyDto) { return this.usersService.validatePasswordResetToken(passwordResetApplyDto.password, passwordResetApplyDto.token); } - - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles('admin') - @Get('pendingStructures') - public getPendingAttachments() { - return this.usersService.getPendingStructures(); - } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index e5e6b9230..b23061603 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -36,6 +36,7 @@ export class UsersService { // Send verification email createUser = await this.verifyUserMail(createUser); createUser.save(); + this.sendAdminStructureValidationMail(); return await this.findOne(createUserDto.email); } @@ -124,6 +125,25 @@ export class UsersService { return user; } + /** + * Generate activation token and send it to user by email, in order to validate + * a new account. + * @param user User + */ + private async sendAdminStructureValidationMail(): Promise<any> { + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.adminStructureClaim.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.adminStructureClaim.json); + + const html = await ejs.renderFile(ejsPath, { + config, + }); + const admins = await this.getAdmins(); + admins.forEach((admin) => { + this.mailerService.send(admin.email, jsonConfig.subject, html); + }); + } + /** * Check that the given token is associated to userId. If it's true, validate user account. * @param userId string @@ -241,11 +261,16 @@ export class UsersService { throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); } + public async getAdmins(): Promise<User[]> { + return this.userModel.find({ role: 1 }).exec(); + } + public async updateStructureLinked(userEmail: string, idStructure: number): Promise<any> { const user = await this.findOne(userEmail, true); if (user) { user.pendingStructuresLink.push(idStructure); user.save(); + this.sendAdminStructureValidationMail(); return user.pendingStructuresLink; } throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); @@ -268,4 +293,32 @@ export class UsersService { }); return structuresPending; } + + /** + * Validate or refuse a pending structure given a email and structure id + */ + public async validatePendingStructure( + userEmail: string, + structureId: number, + validate: boolean + ): Promise<{ userEmail: string; structureId: number }[]> { + const users = await this.findOne(userEmail); + if (!users) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + if (users.pendingStructuresLink.includes(structureId)) { + users.pendingStructuresLink = users.pendingStructuresLink.filter((item) => item !== structureId); + // If it's a validation case, push structureId into validated user structures + if (validate) { + users.structuresLink.push(structureId); + } + await users.save(); + return this.getPendingStructures(); + } else { + throw new HttpException( + 'Cannot validate strucutre. It might have been already validate, or the structure does`nt belong to the user', + HttpStatus.NOT_FOUND + ); + } + } } -- GitLab