From 95608291b11759847484efe9b94a1f81a02a3798 Mon Sep 17 00:00:00 2001 From: Hugo SUBTIL <ext.sopra.husubtil@grandlyon.com> Date: Tue, 16 Feb 2021 12:06:08 +0100 Subject: [PATCH] feat: add temp-user handling for structure join --- src/app.module.ts | 2 + src/configuration/config.ts | 4 + .../mail-templates/tempUserRegistration.ejs | 15 ++++ .../mail-templates/tempUserRegistration.json | 3 + src/structures/structures.controller.ts | 39 ++++++++- src/structures/structures.module.ts | 2 + src/temp-user/dto/create-temp-user.dto.ts | 22 +++++ src/temp-user/dto/temp-user-delete.dto.ts | 9 ++ src/temp-user/temp-user.controller.ts | 18 ++++ src/temp-user/temp-user.interface.ts | 9 ++ src/temp-user/temp-user.module.ts | 14 +++ src/temp-user/temp-user.schema.ts | 21 +++++ src/temp-user/temp-user.service.ts | 87 +++++++++++++++++++ src/users/users.controller.ts | 10 ++- src/users/users.module.ts | 10 ++- src/users/users.service.ts | 25 +++++- 16 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 src/mailer/mail-templates/tempUserRegistration.ejs create mode 100644 src/mailer/mail-templates/tempUserRegistration.json create mode 100644 src/temp-user/dto/create-temp-user.dto.ts create mode 100644 src/temp-user/dto/temp-user-delete.dto.ts create mode 100644 src/temp-user/temp-user.controller.ts create mode 100644 src/temp-user/temp-user.interface.ts create mode 100644 src/temp-user/temp-user.module.ts create mode 100644 src/temp-user/temp-user.schema.ts create mode 100644 src/temp-user/temp-user.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index e0aa57e22..467fe404c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { MailerModule } from './mailer/mailer.module'; import { TclModule } from './tcl/tcl.module'; import { AdminModule } from './admin/admin.module'; import { PostsModule } from './posts/posts.module'; +import { TempUserModule } from './temp-user/temp-user.module'; @Module({ imports: [ ConfigurationModule, @@ -26,6 +27,7 @@ import { PostsModule } from './posts/posts.module'; TclModule, AdminModule, PostsModule, + TempUserModule, ], controllers: [AppController], }) diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 7bb9d6f5a..eef3f2b02 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -37,5 +37,9 @@ export const config = { ejs: 'apticStructureDuplication.ejs', json: 'apticStructureDuplication.json', }, + tempUserRegistration: { + ejs: 'tempUserRegistration.ejs', + json: 'tempUserRegistration.json', + }, }, }; diff --git a/src/mailer/mail-templates/tempUserRegistration.ejs b/src/mailer/mail-templates/tempUserRegistration.ejs new file mode 100644 index 000000000..8422b7dbe --- /dev/null +++ b/src/mailer/mail-templates/tempUserRegistration.ejs @@ -0,0 +1,15 @@ +Bonjour<br /> +<br /> +Vous recevez ce message car vous avez été relié a la stucture <strong><%= name %></strong> sur RES'in, le réseau des +acteurs de l'inclusion numérique de la Métropole de Lyon. Vous pouvez dès maitenant vous créer un compte sur la +plateforme pour accéder a votre structure en +<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/register?id=<%= id %>" + >cliquant ici</a +>. +<br /> +Cordialement, +<br /> +L'équipe RES'in +<br /> +<br /> +Ce mail est un mail automatique. Merci de ne pas y répondre. diff --git a/src/mailer/mail-templates/tempUserRegistration.json b/src/mailer/mail-templates/tempUserRegistration.json new file mode 100644 index 000000000..b344e7953 --- /dev/null +++ b/src/mailer/mail-templates/tempUserRegistration.json @@ -0,0 +1,3 @@ +{ + "subject": "Un compte a été créé pour vous sur le Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" +} diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 6354084b0..2067fb534 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -2,6 +2,9 @@ import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query, U import { ApiParam } from '@nestjs/swagger'; import { Types } from 'mongoose'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { CreateTempUserDto } from '../temp-user/dto/create-temp-user.dto'; +import { TempUser } from '../temp-user/temp-user.schema'; +import { TempUserService } from '../temp-user/temp-user.service'; import { Roles } from '../users/decorators/roles.decorator'; import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard'; import { RolesGuard } from '../users/guards/roles.guard'; @@ -15,7 +18,11 @@ import { StructuresService } from './services/structures.service'; @Controller('structures') export class StructuresController { - constructor(private readonly structureService: StructuresService, private readonly userService: UsersService) {} + constructor( + private readonly structureService: StructuresService, + private readonly userService: UsersService, + private readonly tempUserService: TempUserService + ) {} @Post() public async create(@Body() createStructureDto: CreateStructureDto): Promise<Structure> { @@ -54,7 +61,7 @@ export class StructuresController { @Post(':id/claim') public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<Types.ObjectId[]> { - return this.userService.updateStructureLinked(user.email, idStructure); + return this.userService.updateStructureLinkedClaim(user.email, idStructure); } @Get('count') @@ -95,4 +102,32 @@ export class StructuresController { public async delete(@Param('id') id: string) { return this.structureService.deleteOne(id); } + + @Post(':id/addOwner') + @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) + @Roles('admin') + @ApiParam({ name: 'id', type: String, required: true }) + public async addOwner(@Param('id') id: string, @Body() user: CreateTempUserDto) { + // Get structure name + const structure = await this.structureService.findOne(id); + user.pendingStructuresLink = [Types.ObjectId(id)]; + // If user already exist, use created account + if (await this.userService.verifyUserExist(user.email)) { + return this.userService.updateStructureLinked(user.email, id); + } + // If temp user exist, update it + if (await this.tempUserService.findOne(user.email)) { + return this.tempUserService.updateStructureLinked(user); + } + // If not, create + return this.tempUserService.create(user, structure.structureName); + } + + @Delete(':id/removeOwner') + @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) + @Roles('admin') + @ApiParam({ name: 'id', type: String, required: true }) + public async removeOwner(@Param('id') id: string, @Body() user: User) { + //TODO: remove stucture from user structure list + } } diff --git a/src/structures/structures.module.ts b/src/structures/structures.module.ts index 6494ea3a5..0ec48d708 100644 --- a/src/structures/structures.module.ts +++ b/src/structures/structures.module.ts @@ -1,5 +1,6 @@ import { HttpModule, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { TempUserModule } from '../temp-user/temp-user.module'; import { MailerModule } from '../mailer/mailer.module'; import { UsersModule } from '../users/users.module'; import { Structure, StructureSchema } from './schemas/structure.schema'; @@ -19,6 +20,7 @@ import { StructureType, StructureTypeSchema } from './structure-type/structure-t HttpModule, MailerModule, UsersModule, + TempUserModule, ], controllers: [StructuresController, StructureTypeController], exports: [StructuresService, StructureTypeService], diff --git a/src/temp-user/dto/create-temp-user.dto.ts b/src/temp-user/dto/create-temp-user.dto.ts new file mode 100644 index 000000000..892eb6fca --- /dev/null +++ b/src/temp-user/dto/create-temp-user.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsEmail, IsNotEmpty, IsOptional } from 'class-validator'; +import { Types } from 'mongoose'; + +export class CreateTempUserDto { + @IsNotEmpty() + @IsEmail() + @ApiProperty({ type: String }) + email: string; + + @IsNotEmpty() + @ApiProperty({ type: String }) + name: string; + + @IsNotEmpty() + @ApiProperty({ type: String }) + surname: string; + + @IsArray() + @IsOptional() + pendingStructuresLink?: Types.ObjectId[]; +} diff --git a/src/temp-user/dto/temp-user-delete.dto.ts b/src/temp-user/dto/temp-user-delete.dto.ts new file mode 100644 index 000000000..ac6149d3d --- /dev/null +++ b/src/temp-user/dto/temp-user-delete.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty } from 'class-validator'; + +export class DeleteTempUserDto { + @IsNotEmpty() + @IsEmail() + @ApiProperty({ type: String }) + email: string; +} diff --git a/src/temp-user/temp-user.controller.ts b/src/temp-user/temp-user.controller.ts new file mode 100644 index 000000000..7ff27ce27 --- /dev/null +++ b/src/temp-user/temp-user.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get, HttpException, HttpStatus, Param } from '@nestjs/common'; +import { ApiParam } from '@nestjs/swagger'; +import { TempUserService } from './temp-user.service'; + +@Controller('temp-user') +export class TempUserController { + constructor(private readonly tempUserSercice: TempUserService) {} + + @Get(':id') + @ApiParam({ name: 'id', type: String, required: true }) + public async getTempUser(@Param('id') id: string) { + const user = await this.tempUserSercice.findById(id); + if (!user) { + throw new HttpException('User does not exists', HttpStatus.BAD_REQUEST); + } + return user; + } +} diff --git a/src/temp-user/temp-user.interface.ts b/src/temp-user/temp-user.interface.ts new file mode 100644 index 000000000..b802cf3dd --- /dev/null +++ b/src/temp-user/temp-user.interface.ts @@ -0,0 +1,9 @@ +import { Document, Types } from 'mongoose'; + +export interface ITempUser extends Document { + readonly _id: string; + email: string; + name: string; + surname: string; + pendingStructuresLink: Types.ObjectId[]; +} diff --git a/src/temp-user/temp-user.module.ts b/src/temp-user/temp-user.module.ts new file mode 100644 index 000000000..9b55281e4 --- /dev/null +++ b/src/temp-user/temp-user.module.ts @@ -0,0 +1,14 @@ +import { HttpModule, Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { TempUser, TempUserSchema } from './temp-user.schema'; +import { TempUserService } from './temp-user.service'; +import { TempUserController } from './temp-user.controller'; +import { MailerModule } from '../mailer/mailer.module'; + +@Module({ + imports: [MongooseModule.forFeature([{ name: TempUser.name, schema: TempUserSchema }]), HttpModule, MailerModule], + providers: [TempUserService], + exports: [TempUserService], + controllers: [TempUserController], +}) +export class TempUserModule {} diff --git a/src/temp-user/temp-user.schema.ts b/src/temp-user/temp-user.schema.ts new file mode 100644 index 000000000..c8777e3ef --- /dev/null +++ b/src/temp-user/temp-user.schema.ts @@ -0,0 +1,21 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document, Types } from 'mongoose'; + +export type TempUserDocument = TempUser & Document; + +@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) +export class TempUser { + @Prop({ required: true }) + email: string; + + @Prop({ required: true }) + name: string; + + @Prop({ required: true }) + surname: string; + + @Prop({ default: null }) + pendingStructuresLink: Types.ObjectId[]; +} + +export const TempUserSchema = SchemaFactory.createForClass(TempUser); diff --git a/src/temp-user/temp-user.service.ts b/src/temp-user/temp-user.service.ts new file mode 100644 index 000000000..8d3eba399 --- /dev/null +++ b/src/temp-user/temp-user.service.ts @@ -0,0 +1,87 @@ +import { HttpException, HttpService, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { MailerService } from '../mailer/mailer.service'; +import { CreateTempUserDto } from './dto/create-temp-user.dto'; +import { TempUser, TempUserDocument } from './temp-user.schema'; +import * as ejs from 'ejs'; +import { ITempUser } from './temp-user.interface'; + +@Injectable() +export class TempUserService { + constructor( + private readonly httpService: HttpService, + private readonly mailerService: MailerService, + @InjectModel(TempUser.name) private tempUserModel: Model<ITempUser> + ) {} + + public async create(createTempUser: CreateTempUserDto, structureName: string): Promise<TempUser> { + const userInDb = await this.findOne(createTempUser.email); + if (userInDb) { + throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); + } + const createUser = new this.tempUserModel(createTempUser); + // Send email + this.sendUserMail(createUser, structureName); + createUser.save(); + return await this.findOne(createTempUser.email); + } + + public async findOne(mail: string): Promise<TempUser | undefined> { + return this.tempUserModel.findOne({ email: mail }).exec(); + } + + public async findById(id: string): Promise<TempUser | undefined> { + return this.tempUserModel.findById(Types.ObjectId(id)).exec(); + } + + public async delete(mail: string): Promise<TempUser> { + const userInDb = await this.findOne(mail); + if (!userInDb) { + throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); + } + this.tempUserModel.deleteOne({ email: mail }).exec(); + return userInDb; + } + + public async updateStructureLinked(createTempUser: CreateTempUserDto): Promise<TempUser> { + const userInDb = await this.tempUserModel + .find({ + $and: [ + { + email: createTempUser.email, + }, + { + pendingStructuresLink: { $in: [createTempUser.pendingStructuresLink[0]] }, + }, + ], + }) + .exec(); + if (userInDb.length > 0) { + throw new HttpException('User already linked', HttpStatus.UNPROCESSABLE_ENTITY); + } + return this.tempUserModel + .updateOne( + { email: createTempUser.email }, + { $push: { pendingStructuresLink: createTempUser.pendingStructuresLink[0] } } + ) + .exec(); + } + + /** + * Send email in order to tell the user that an account is alreday fill with his structure info. + * @param user User + */ + private async sendUserMail(user: ITempUser, structureName: string): Promise<any> { + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.tempUserRegistration.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.tempUserRegistration.json); + + const html = await ejs.renderFile(ejsPath, { + config, + id: user._id, + name: structureName, + }); + this.mailerService.send(user.email, jsonConfig.subject, html); + } +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index a3d450e51..a16fca191 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -7,10 +7,11 @@ 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 { TempUserService } from '../temp-user/temp-user.service'; @Controller('users') export class UsersController { - constructor(private usersService: UsersService) {} + constructor(private usersService: UsersService, private tempUserService: TempUserService) {} @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT') @@ -33,7 +34,12 @@ export class UsersController { } const user = await this.usersService.create(createUserDto); if (structureId) { - this.usersService.updateStructureLinked(createUserDto.email, structureId); + this.usersService.updateStructureLinkedClaim(createUserDto.email, structureId); + } + // Remove temp user if exist + const tempUser = await this.tempUserService.findOne(createUserDto.email); + if (tempUser) { + this.tempUserService.delete(createUserDto.email); } return user; } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index ae73bfe01..2741bbd41 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,12 +1,18 @@ -import { Module } from '@nestjs/common'; +import { HttpModule, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { User, UserSchema } from './schemas/user.schema'; import { MailerModule } from '../mailer/mailer.module'; +import { TempUserModule } from '../temp-user/temp-user.module'; @Module({ - imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), MailerModule], + imports: [ + MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), + MailerModule, + HttpModule, + TempUserModule, + ], providers: [UsersService], exports: [UsersService], controllers: [UsersController], diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 65c965aac..0763ff83d 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -320,13 +320,18 @@ export class UsersService { return false; } - public async updateStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + public async updateStructureLinkedClaim(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure); + this.sendAdminStructureValidationMail(); + return stucturesLinked; + } + + public async updatePendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { const user = await this.findOne(userEmail, true); if (user) { if (!user.pendingStructuresLink.includes(Types.ObjectId(idStructure))) { user.pendingStructuresLink.push(Types.ObjectId(idStructure)); user.save(); - this.sendAdminStructureValidationMail(); return user.pendingStructuresLink; } throw new HttpException('User already claimed this structure', HttpStatus.NOT_FOUND); @@ -334,6 +339,22 @@ export class UsersService { throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); } + public async updateStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + const user = await this.findOne(userEmail, true); + if (user) { + if ( + !user.pendingStructuresLink.includes(Types.ObjectId(idStructure)) && + !user.structuresLink.includes(Types.ObjectId(idStructure)) + ) { + user.structuresLink.push(Types.ObjectId(idStructure)); + user.save(); + return user.structuresLink; + } + throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND); + } + throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); + } + /** * Return all pending attachments of all profiles */ -- GitLab