diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b8ca9028651014a25b02e06c12425dee70ab800 --- /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 0000000000000000000000000000000000000000..db2e4620c3abd8aeecb8f30d6a66a17e5d1ba244 --- /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 0000000000000000000000000000000000000000..3376e00f618f654dbdb6213a2cfd48ddb8e99cb2 --- /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 0000000000000000000000000000000000000000..5e5e153df03a8437915bb59ff2861c9366e40f02 --- /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 0000000000000000000000000000000000000000..796f9fd1a19a12cbc184a3364dbebe88e2165365 --- /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 0000000000000000000000000000000000000000..04936833a26fea61d590dac33f3f682492f7c9dc --- /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 d3a7ebdbbc56ff9f03b326d78405b5cb81008137..0d49be79613c4a4a01487ac0ab4110fe19006ba7 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 b6d29acca6ec81c264ea73c4c482790a914a3c36..34c5a4a24c2560003e0a91754e1c5cf50b8dcb2b 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 0000000000000000000000000000000000000000..12ba539835a44a9a609bd7d1937d45ec48e09474 --- /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 0000000000000000000000000000000000000000..bad53f08f4c0debbe28b988b65f37b6a3902ddb2 --- /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 4c6ad99bb0174078b3393819b40259d7c7ec71e0..e9b08fe970220922201f068a867c9fdfea976968 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 85d241a5f62339cbdd2768667f286ad75dd2ec46..d1b9a7e5382284a27e399491159d792f02105468 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 d78059dc5688afb2b2291e872847d613dc045df5..9c8c2c31b19ad0fec488cc11fd3ec1e53976d93a 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 e5e6b923077ea9ccc842cdc7ef577f80b11549e1..b23061603d63e1b48a63cb82866922be5b79b252 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 + ); + } + } }