diff --git a/src/configuration/config.dev.ts b/src/configuration/config.dev.ts index 30d4a3f124d3f64e1c4427164e985e193a07535f..90917c6903316ef855ee1e2d21e7b93dcb7aa76a 100644 --- a/src/configuration/config.dev.ts +++ b/src/configuration/config.dev.ts @@ -7,31 +7,4 @@ export const configDev = { from: 'inclusionnumerique@grandlyon.com', from_name: 'Réseau des acteurs de la médiation numérique', replyTo: 'inclusionnumerique@grandlyon.com', - templates: { - directory: './src/mailer/mail-templates', - verify: { - ejs: 'verify.ejs', - json: 'verify.json', - }, - changeEmail: { - ejs: 'changeEmail.ejs', - json: 'changeEmail.json', - }, - resetPassword: { - ejs: 'resetPassword.ejs', - json: 'resetPassword.json', - }, - adminStructureClaim: { - ejs: 'adminStructureClaim.ejs', - json: 'adminStructureClaim.json', - }, - structureClaimValidation: { - ejs: 'structureClaimValidation.ejs', - json: 'structureClaimValidation.json', - }, - structureOutdatedInfo: { - ejs: 'structureOutdatedInfo.ejs', - json: 'structureOutdatedInfo.json', - }, - }, }; diff --git a/src/configuration/config.prod.ts b/src/configuration/config.prod.ts index 19167f8c124329bc4acf99b1d1ce7c3dc84ed032..d9333dd6499c2498a7853abf5ad97b8e1a2c849c 100644 --- a/src/configuration/config.prod.ts +++ b/src/configuration/config.prod.ts @@ -7,31 +7,4 @@ export const configProd = { from: 'inclusionnumerique@grandlyon.com', from_name: 'Réseau des acteurs de la médiation numérique', replyTo: 'inclusionnumerique@grandlyon.com', - templates: { - directory: './src/mailer/mail-templates', - verify: { - ejs: 'verify.ejs', - json: 'verify.json', - }, - changeEmail: { - ejs: 'changeEmail.ejs', - json: 'changeEmail.json', - }, - resetPassword: { - ejs: 'resetPassword.ejs', - json: 'resetPassword.json', - }, - adminStructureClaim: { - ejs: 'adminStructureClaim.ejs', - json: 'adminStructureClaim.json', - }, - structureClaimValidation: { - ejs: 'structureClaimValidation.ejs', - json: 'structureClaimValidation.json', - }, - structureOutdatedInfo: { - ejs: 'structureOutdatedInfo.ejs', - json: 'structureOutdatedInfo.json', - }, - }, }; diff --git a/src/configuration/config.ts b/src/configuration/config.ts index eef3f2b02b8461813160d4700f18229a7d517dfe..e9d940409b5b8b927e138bb8b33a9e2ff6935743 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -41,5 +41,9 @@ export const config = { ejs: 'tempUserRegistration.ejs', json: 'tempUserRegistration.json', }, + structureJoinRequest: { + ejs: 'structureJoinRequest.ejs', + json: 'structureJoinRequest.json', + }, }, }; diff --git a/src/configuration/configuration.service.ts b/src/configuration/configuration.service.ts index 13b4f5e4d827e55bc85ba4cacb6dc54ada5c887c..ca6f9e6c44a78e2259c3b3e70c8a5168d3d30d95 100644 --- a/src/configuration/configuration.service.ts +++ b/src/configuration/configuration.service.ts @@ -10,9 +10,11 @@ export class ConfigurationService { // Initializing conf with values from var env if (process.env.NODE_ENV && process.env.NODE_ENV === 'production') { this._config = configProd; + this._config.templates = config.templates; // Add mail templates Logger.log('App started with production conf', 'ConfigurationService'); } else if (process.env.NODE_ENV && process.env.NODE_ENV === 'dev') { this._config = configDev; + this._config.templates = config.templates; // Add mail templates Logger.log('App started with dev conf', 'ConfigurationService'); } else { this._config = config; diff --git a/src/mailer/mail-templates/structureJoinRequest.ejs b/src/mailer/mail-templates/structureJoinRequest.ejs new file mode 100644 index 0000000000000000000000000000000000000000..550665e04bb48c9f796a21b97fef4dfc11407107 --- /dev/null +++ b/src/mailer/mail-templates/structureJoinRequest.ejs @@ -0,0 +1,21 @@ +Bonjour<br /> +<br /> +Vous recevez ce message car <strong><%= surname %></strong> <strong><%= name %></strong> demande a rejoindre votre +stucture <strong><%= structureName %></strong> sur RES'in, le réseau des acteurs de l'inclusion numérique de la +Métropole de Lyon. Vous pouvez dès maintenant valider la demande en +<a + href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/join?id=<%= id %>&userId=<%= userId %>&status=true" + >cliquant ici</a +> +ou refuser la demande +<a + href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/join?id=<%= id %>&userId=<%= userId %>&status=false" + >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/structureJoinRequest.json b/src/mailer/mail-templates/structureJoinRequest.json new file mode 100644 index 0000000000000000000000000000000000000000..8b756e80dda646b74494d044a1c027c48294ac34 --- /dev/null +++ b/src/mailer/mail-templates/structureJoinRequest.json @@ -0,0 +1,3 @@ +{ + "subject": "Un acteur demande a rejoindre votre structure, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" +} diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index 83c48a25995b9c7ce714c8dce9635e69d4aa3e1a..6a67a5367fbaee90330da8b911caf7501f75819d 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -11,6 +11,7 @@ import { User } from '../../users/schemas/user.schema'; import { MailerService } from '../../mailer/mailer.service'; import { Cron, CronExpression } from '@nestjs/schedule'; import { DateTime } from 'luxon'; +import { IUser } from '../../users/interfaces/user.interface'; @Injectable() export class StructuresService { @@ -113,7 +114,7 @@ export class StructuresService { return this.findOne(idStructure); } - public async findOne(idParam: string): Promise<Structure> { + public async findOne(idParam: string): Promise<StructureDocument> { return await this.structureModel.findById(Types.ObjectId(idParam)).exec(); } /** @@ -284,7 +285,7 @@ export class StructuresService { } /** - * Generate activation token and send it to user by email, in order to validate + * Send an email to prevent outdated * a new account. * @param user User */ @@ -301,6 +302,34 @@ export class StructuresService { this.mailerService.send(userEmail, jsonConfig.subject, html); } + /** + * Send an email to structure owner's in order to accept or decline a join request + * @param user User + */ + public async sendStructureJoinRequest(user: IUser, structure: StructureDocument): Promise<any> { + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureJoinRequest.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureJoinRequest.json); + + const html = await ejs.renderFile(ejsPath, { + config, + structureName: structure.structureName, + name: user.name, + surname: user.surname, + id: structure._id, + userId: user._id, + }); + const owners = await this.getOwners(structure._id); + owners.forEach((owner) => { + this.mailerService.send(owner.email, jsonConfig.subject, html); + }); + } + + private async getOwners(structureId: string): Promise<IUser[]> { + // Get owners of outdated structures + return this.userService.getStructureOwners(structureId); + } + public async updateAccountVerified(idStructure: string, emailUser: string): Promise<Structure> { const user = await this.userService.findOne(emailUser); const structureLinked = await this.findOne(user.structuresLink[0].toHexString()); diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 2067fb53460239d16910d99f9bd3b853dcd1dd3a..4a58d7162c63babafe0766b27559af953a335def 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -1,4 +1,17 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + ParseIntPipe, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; import { ApiParam } from '@nestjs/swagger'; import { Types } from 'mongoose'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @@ -107,9 +120,12 @@ export class StructuresController { @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) @Roles('admin') @ApiParam({ name: 'id', type: String, required: true }) - public async addOwner(@Param('id') id: string, @Body() user: CreateTempUserDto) { + public async addOwner(@Param('id') id: string, @Body() user: CreateTempUserDto): Promise<any> { // Get structure name const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } user.pendingStructuresLink = [Types.ObjectId(id)]; // If user already exist, use created account if (await this.userService.verifyUserExist(user.email)) { @@ -123,11 +139,79 @@ export class StructuresController { return this.tempUserService.create(user, structure.structureName); } + @Post(':id/join') + @ApiParam({ name: 'id', type: String, required: true }) + public async join(@Param('id') id: string, @Body() user: User): Promise<any> { + // Get structure name + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + // Get user and add pending structure + const userFromDb = await this.userService.findOne(user.email); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + // If user has not already request it, send owner's validation email + if (!userFromDb.pendingStructuresLink.includes(Types.ObjectId(id))) { + userFromDb.pendingStructuresLink.push(Types.ObjectId(id)); + userFromDb.save(); + // Send structure owner's an email + this.structureService.sendStructureJoinRequest(userFromDb, structure); + } + } + + @Post(':id/join/:userId/:status') + @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) + @ApiParam({ name: 'id', type: String, required: true }) + @ApiParam({ name: 'userId', type: String, required: true }) + @ApiParam({ name: 'status', type: String, required: true }) + public async joinValidation( + @Param('id') id: string, + @Param('status') status: string, + @Param('userId') userId: string + ): Promise<any> { + // Get structure name + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + + // Get user and add pending structure + const userFromDb = await this.userService.findById(userId); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + + if (!userFromDb.pendingStructuresLink.includes(Types.ObjectId(id))) { + throw new HttpException('User not linked to structure', HttpStatus.NOT_FOUND); + } + + if (status === 'true') { + // Accept + await this.userService.updateStructureLinked(userFromDb.email, id); + await this.userService.removeFromPendingStructureLinked(userFromDb.email, id); + } else { + // Refuse + this.userService.removeFromPendingStructureLinked(userFromDb.email, id); + } + } + @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 + public async removeOwner(@Param('id') id: string, @Body() user: User): Promise<any> { + // Get structure + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + // Get user + const userFromDb = await this.userService.findOne(user.email); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + this.userService.removeFromStructureLinked(userFromDb.email, id); } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 0763ff83d7822a9741928f7d2b03a1ef9aa85540..49aa23df94f0a63cf103c2c4a9466f93b3505d71 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -312,6 +312,10 @@ export class UsersService { return this.userModel.findOne({ structuresLink: Types.ObjectId(structureId) }).exec(); } + public getStructureOwners(structureId: string): Promise<IUser[]> { + return this.userModel.find({ structuresLink: Types.ObjectId(structureId) }).exec(); + } + public async isUserAlreadyClaimedStructure(structureId: string, userEmail: string): Promise<boolean> { const user = await this.findOne(userEmail, true); if (user) { @@ -331,7 +335,7 @@ export class UsersService { if (user) { if (!user.pendingStructuresLink.includes(Types.ObjectId(idStructure))) { user.pendingStructuresLink.push(Types.ObjectId(idStructure)); - user.save(); + await user.save(); return user.pendingStructuresLink; } throw new HttpException('User already claimed this structure', HttpStatus.NOT_FOUND); @@ -339,15 +343,42 @@ export class UsersService { throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); } + public async removeFromPendingStructureLinked(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 = user.pendingStructuresLink.filter((structureId) => { + return structureId === Types.ObjectId(idStructure); + }); + await user.save(); + return user.pendingStructuresLink; + } + throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND); + } + 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)) - ) { + if (!user.structuresLink.includes(Types.ObjectId(idStructure))) { user.structuresLink.push(Types.ObjectId(idStructure)); - user.save(); + await 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); + } + + public async removeFromStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + const user = await this.findOne(userEmail, true); + if (user) { + if (user.structuresLink.includes(Types.ObjectId(idStructure))) { + user.structuresLink = user.structuresLink.filter((structureId) => { + return structureId == Types.ObjectId(idStructure); + }); + await user.save(); return user.structuresLink; } throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND);