diff --git a/src/app.module.ts b/src/app.module.ts index e0aa57e2278e56f027bcfe0d0db969e5936708c8..467fe404c31f6fdeadc948aec96bdf136aa676f3 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 7bb9d6f5a8b02c9fc654caa84f5cb74a223c99e9..eef3f2b02b8461813160d4700f18229a7d517dfe 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 0000000000000000000000000000000000000000..8422b7dbefde902844d6970d16a0b19282321a92 --- /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 0000000000000000000000000000000000000000..b344e7953f3643b216eec56a385e464f4d6a0de6 --- /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 6354084b00788cc340b4c4acfba0e4a7d89c056f..2067fb53460239d16910d99f9bd3b853dcd1dd3a 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 6494ea3a5eed4386ed251679c6e5b45a66e88804..0ec48d708808d0a1a3fdf495a037c99ba14bb9cf 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 0000000000000000000000000000000000000000..892eb6fca9e723f5b3be341d7e3c4cc90cc009de --- /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 0000000000000000000000000000000000000000..ac6149d3d67d378dc0fd6641652a636148f1782c --- /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 0000000000000000000000000000000000000000..7ff27ce27d4aef70c96e2b3a3433913977259e86 --- /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 0000000000000000000000000000000000000000..b802cf3dd6c66a907bf923f9eaa678c15e7e9d81 --- /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 0000000000000000000000000000000000000000..9b55281e49839559323398587a13a487cb462ea6 --- /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 0000000000000000000000000000000000000000..c8777e3ef81fba888fc2185c754bcdf8d9a435b2 --- /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 0000000000000000000000000000000000000000..8d3eba3993df83ff996aa597ea74637907975623 --- /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 a3d450e51a4c4fdc4bbb6bce6e46234165bf65a8..a16fca1915a89d913e885dd7772e321040f75ff9 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 ae73bfe01263691656b98892941287ea85642e23..2741bbd41a798f060a82c203249b9064dabbaa9b 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 65c965aac9065e6cffaadb5fbba9e68da2940bf7..0763ff83d7822a9741928f7d2b03a1ef9aa85540 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 */