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