diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts index 3cde011013f5fd0980a0ba7952f8fa120cd89fae..97ba73ce36e4fb5f24927bfea36ac140d918162d 100644 --- a/src/admin/admin.controller.spec.ts +++ b/src/admin/admin.controller.spec.ts @@ -24,6 +24,7 @@ import { JobsService } from '../users/services/jobs.service'; import { UsersService } from '../users/services/users.service'; import { AdminController } from './admin.controller'; import { AdminService } from './admin.service'; +import { PendingStructureDto } from './dto/pending-structure.dto'; describe('AdminController', () => { let controller: AdminController; @@ -62,7 +63,13 @@ describe('AdminController', () => { mergeJob: jest.fn(), deleteInvalidJob: jest.fn(), }; - + const pendingStructureTest: PendingStructureDto = { + structureId: new Types.ObjectId('6093ba0e2ab5775cfc01ed3e'), + structureName: 'test', + userEmail: 'jean.paul@mii.com', + createdAt: new Date('2021-02-02T10:07:48.000Z'), + updatedAt: new Date('2021-03-02T10:07:48.000Z'), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ConfigurationModule, HttpModule, SearchModule], @@ -133,25 +140,11 @@ describe('AdminController', () => { describe('Pending structures validation', () => { it('should validate pending structure', async () => { - const pendingStructureTest = { - structureId: '6093ba0e2ab5775cfc01ed3e', - structureName: 'test', - userEmail: 'jean.paul@mii.com', - createdAt: new Date('2021-02-02T10:07:48.000Z'), - updatedAt: new Date('2021-03-02T10:07:48.000Z'), - }; expect((await controller.validatePendingStructure(pendingStructureTest)).length).toBe(2); expect(Object.keys((await controller.validatePendingStructure(pendingStructureTest))[0]).length).toBe(5); }); it('should get structure does not exist', async () => { - const pendingStructureTest = { - structureId: '1093ba0e2ab5775cfc01z2ki', - structureName: 'test', - userEmail: 'jean.paul@mii.com', - createdAt: new Date('2021-02-02T10:07:48.000Z'), - updatedAt: new Date('2021-03-02T10:07:48.000Z'), - }; try { await controller.validatePendingStructure(pendingStructureTest); } catch (e) { @@ -163,25 +156,11 @@ describe('AdminController', () => { describe('Pending structures cancel', () => { it('should refuse pending structure', async () => { - const pendingStructureTest = { - structureId: '6093ba0e2ab5775cfc01ed3e', - structureName: 'test', - userEmail: 'jean.paul@mii.com', - createdAt: new Date('2021-02-02T10:07:48.000Z'), - updatedAt: new Date('2021-03-02T10:07:48.000Z'), - }; expect((await controller.refusePendingStructure(pendingStructureTest)).length).toBe(2); expect(Object.keys((await controller.refusePendingStructure(pendingStructureTest))[0]).length).toBe(5); }); it('should get structure does not exist', async () => { - const pendingStructureTest = { - structureId: '1093ba0e2ab5775cfc01z2ki', - structureName: 'test', - userEmail: 'jean.paul@mii.com', - createdAt: new Date('2021-02-02T10:07:48.000Z'), - updatedAt: new Date('2021-03-02T10:07:48.000Z'), - }; try { await controller.refusePendingStructure(pendingStructureTest); } catch (e) { diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 5436bf7d25fb1d78e3b7ef288e494a213d8319c8..983783bbb9b7660a4c87b3806187bc17d53216de 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -52,7 +52,6 @@ export class AdminController { pendingStructure.map(async (structure) => { const structureDocument = await this.structuresService.findOne(structure.structureId); structure.structureName = structureDocument.structureName; - structure.createdAt = structureDocument.createdAt; structure.updatedAt = structureDocument.updatedAt; return structure; }) @@ -135,13 +134,13 @@ export class AdminController { @Post('validatePendingStructure') @ApiOperation({ description: 'Validate structure ownership' }) public async validatePendingStructure(@Body() pendingStructureDto: PendingStructureDto) { - const structure = await this.structuresService.findOne(pendingStructureDto.structureId); + const structure = await this.structuresService.findOne(pendingStructureDto.structureId.toString()); if (!structure || structure.deletedAt) { throw new HttpException('Structure does not exist', HttpStatus.NOT_FOUND); } await this.usersService.validatePendingStructure( pendingStructureDto.userEmail, - pendingStructureDto.structureId, + pendingStructureDto.structureId.toString(), structure.structureName, true ); @@ -161,13 +160,13 @@ export class AdminController { @Post('rejectPendingStructure') @ApiOperation({ description: 'Refuse structure ownership' }) public async refusePendingStructure(@Body() pendingStructureDto: PendingStructureDto) { - const structure = await this.structuresService.findOne(pendingStructureDto.structureId); + const structure = await this.structuresService.findOne(pendingStructureDto.structureId.toString()); if (!structure || structure.deletedAt) { throw new HttpException('Structure does not exist', HttpStatus.NOT_FOUND); } await this.usersService.validatePendingStructure( pendingStructureDto.userEmail, - pendingStructureDto.structureId, + pendingStructureDto.structureId.toString(), structure.structureName, false ); diff --git a/src/admin/dto/pending-structure.dto.ts b/src/admin/dto/pending-structure.dto.ts index b45f01117a6a900e2d1810e33d921aeaafec5f21..757bf2bc24843e39193f65bada01c7a5654e8e81 100644 --- a/src/admin/dto/pending-structure.dto.ts +++ b/src/admin/dto/pending-structure.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsMongoId, IsNotEmpty, IsString } from 'class-validator'; +import { Types } from 'mongoose'; export class PendingStructureDto { @IsNotEmpty() @@ -9,8 +10,8 @@ export class PendingStructureDto { @IsNotEmpty() @IsMongoId() - @ApiProperty({ type: String }) - readonly structureId: string; + @ApiProperty({ type: Types.ObjectId }) + readonly structureId: Types.ObjectId; @IsNotEmpty() @IsString() @@ -18,7 +19,7 @@ export class PendingStructureDto { structureName: string; @ApiProperty({ type: Date }) - updatedAt: Date; + updatedAt?: Date; @ApiProperty({ type: Date }) createdAt: Date; diff --git a/src/mailer/mail-templates/structureJoinRequest.ejs b/src/mailer/mail-templates/structureJoinRequest.ejs index ce6bce858f1893b640153135ec3e65b6b393c9c3..7c77f5d06f8686adacd9f9b55ad102e5c79cb967 100644 --- a/src/mailer/mail-templates/structureJoinRequest.ejs +++ b/src/mailer/mail-templates/structureJoinRequest.ejs @@ -1,14 +1,14 @@ Bonjour,<br /> <br /> -<%= name %> <%= surname %> indique travailler au sein de votre structure <%= structureName %>.<br /> -S'il s'agit d'une erreur, merci de nous le signaler en cliquant sur le lien ci-dessous. -<br /> -<br /> -<br /> - -<div style="text-align: center"> - <a - href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/exclude?id=<%= id %>&userId=<%= userId %>" - >Signaler une erreur d'appartenance à cette structure</a - > -</div> +Vous recevez ce message car <strong><%= surname %></strong> <strong><%= name %></strong> indique travailler au sein de +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 +<a + href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/join-validation?id=<%= id %>&userId=<%= userId %>&status=true&token=<%= validationToken %>" + >valider la demande</a +> +ou +<a + href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/join-validation?id=<%= id %>&userId=<%= userId %>&status=false&token=<%= validationToken %>" + >refuser la demande</a +>. diff --git a/src/mailer/mail-templates/structureJoinRequest.json b/src/mailer/mail-templates/structureJoinRequest.json index 23e2f607f8f7b00f73f9c39e19021af222fb33d0..f5f38230d30e32580935c7bb4cbbe4d403d40baf 100644 --- a/src/mailer/mail-templates/structureJoinRequest.json +++ b/src/mailer/mail-templates/structureJoinRequest.json @@ -1,3 +1,3 @@ { - "subject": "Un acteur a rejoint votre structure, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" + "subject": "Un acteur demande à rejoindre votre structure, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" } diff --git a/src/migrations/scripts/1653297328930-empty-pendingStructuresLink.ts b/src/migrations/scripts/1669987046611-pendingstructuresreset.ts similarity index 100% rename from src/migrations/scripts/1653297328930-empty-pendingStructuresLink.ts rename to src/migrations/scripts/1669987046611-pendingstructuresreset.ts diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index e458a70bd521cbffb1d4b4e37cd8079da2fd286a..cd34a96faacf35e9773ff4e33ccfb746928bd63d 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -8,6 +8,7 @@ import * as _ from 'lodash'; import { DateTime } from 'luxon'; import { DocumentDefinition, FilterQuery, Model, Types } from 'mongoose'; import { map, Observable, tap } from 'rxjs'; +import { PendingStructureDto } from '../../admin/dto/pending-structure.dto'; import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto'; import { Categories } from '../../categories/schemas/categories.schema'; import { Module } from '../../categories/schemas/module.class'; @@ -787,9 +788,18 @@ export class StructuresService { .exists(false) .exec(); + const pendingStructures: PendingStructureDto[] = (await this.userService.getPendingStructures()) as PendingStructureDto[]; + const structurIds = pendingStructures.map((pending) => pending.structureId); structures.forEach((structure) => { - this.logger.debug(`delete structure : ${structure.structureName} (${structure._id})`); - this.deleteOne(structure); + if (structurIds.includes(structure.id)) { + this.logger.debug(`cancel structure soft-delete : ${structure.structureName} (${structure._id})`); + this.structureModel.findByIdAndUpdate(new Types.ObjectId(structure.id), { + toBeDeletedAt: null, + }); + } else { + this.logger.debug(`delete structure : ${structure.structureName} (${structure._id})`); + this.deleteOne(structure); + } }); } @@ -851,10 +861,14 @@ export class StructuresService { } /** - * Send an email to structure owners in order to accept or decline a join request + * Send an email to structure owners and admin in order to accept or decline a join request * @param user User */ - public async sendStructureJoinRequest(user: IUser, structure: StructureDocument): Promise<void> { + public async sendStructureJoinRequest( + user: IUser, + structure: StructureDocument, + validationToken: string + ): Promise<void> { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureJoinRequest.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureJoinRequest.json); @@ -866,13 +880,19 @@ export class StructuresService { surname: user.surname, id: structure._id, userId: user._id, + validationToken: validationToken, }); const owners = await this.getOwners(structure._id); - owners.forEach((owner) => { + for (const owner of owners) { if (!owner._id.equals(user._id)) { this.mailerService.send(owner.email, jsonConfig.subject, html); } - }); + } + //TODO handle case if admin is owner and receive mail 2 times + const admins = await this.userService.getAdmins(); + for (const admin of admins) { + this.mailerService.send(admin.email, jsonConfig.subject, html); + } } private async getOwners(structureId: string): Promise<IUser[]> { diff --git a/src/structures/structures.controller.spec.ts b/src/structures/structures.controller.spec.ts index ec56a40844fc2873166300a0012fed19a9d4cd06..d4bd2b3ff6cec6e2826ce049297428a5033a802f 100644 --- a/src/structures/structures.controller.spec.ts +++ b/src/structures/structures.controller.spec.ts @@ -1,6 +1,7 @@ import { HttpService } from '@nestjs/axios'; import { HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { Types } from 'mongoose'; import { CategoriesServiceMock } from '../../test/mock/services/categories.mock.service'; import { HttpServiceMock } from '../../test/mock/services/http.mock.service'; import { StructuresServiceMock } from '../../test/mock/services/structures.mock.service'; @@ -82,7 +83,7 @@ describe('AuthController', () => { it('should update structure', async () => { const structureService = new StructuresServiceMock(); - const structure = structureService.findOne('6093ba0e2ab5775cfc01ed3e'); + const structure = structureService.findOne(new Types.ObjectId('6093ba0e2ab5775cfc01ed3e')); const structureId = '1'; const res = await controller.update(structureId, { @@ -246,43 +247,43 @@ describe('AuthController', () => { expect(res).toBeTruthy(); }); - it('should join user', async () => { - const userMock = new UsersServiceMock(); - const user = userMock.findOne('pauline.dupont@mii.com'); - let res = controller.join('6093ba0e2ab5775cfc01ed3e', { - phone: null, - resetPasswordToken: null, - changeEmailToken: null, - newEmail: null, - pendingStructuresLink: null, - structuresLink: null, - structureOutdatedMailSent: null, - personalOffers: null, - email: user.email, - name: user.name, - surname: user.surname, - emailVerified: true, - createdAt: new Date('2022-05-25T09:48:28.824Z'), - password: user.password, - validationToken: null, - role: null, - employer: { - name: 'test', - validated: true, - }, - job: { - name: 'test', - validated: true, - hasPersonalOffer: false, - }, - unattachedSince: null, - }); - expect(res).toBeTruthy(); - res = controller.join('', null); - expect(res).toBeTruthy(); - res = controller.join('6093ba0e2ab5775cfc01ed3e', null); - expect(res).toBeTruthy(); - }); + // it('should join user', async () => { + // const userMock = new UsersServiceMock(); + // const user = userMock.findOne('pauline.dupont@mii.com'); + // let res = controller.join('6093ba0e2ab5775cfc01ed3e', { + // phone: null, + // resetPasswordToken: null, + // changeEmailToken: null, + // newEmail: null, + // pendingStructuresLink: null, + // structuresLink: null, + // structureOutdatedMailSent: null, + // personalOffers: null, + // email: user.email, + // name: user.name, + // surname: user.surname, + // emailVerified: true, + // createdAt: new Date('2022-05-25T09:48:28.824Z'), + // password: user.password, + // validationToken: null, + // role: null, + // employer: { + // name: 'test', + // validated: true, + // }, + // job: { + // name: 'test', + // validated: true, + // hasPersonalOffer: false, + // }, + // unattachedSince: null, + // }); + // expect(res).toBeTruthy(); + // res = controller.join('', null); + // expect(res).toBeTruthy(); + // res = controller.join('6093ba0e2ab5775cfc01ed3e', null); + // expect(res).toBeTruthy(); + // }); it('should remove user from struct', async () => { const res = controller.removeOwner('6093ba0e2ab5775cfc01ed3e', 'tsfsf6296'); diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 8749f1d07726bd3e7b33c6175e9b9f058c2d1833..9385e2d525474907eeb7e5ee8d074da1deeff24a 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -26,6 +26,7 @@ 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'; +import { pendingStructuresLink } from '../users/interfaces/pendingStructure'; import { IUser } from '../users/interfaces/user.interface'; import { User } from '../users/schemas/user.schema'; import { UsersService } from '../users/services/users.service'; @@ -136,7 +137,7 @@ export class StructuresController { } @Post(':id/claim') - public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<Types.ObjectId[]> { + public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<pendingStructuresLink[]> { const structure = await this.structureService.findOne(idStructure); return this.userService.updateStructureLinkedClaim(user.email, idStructure, structure); } @@ -210,7 +211,8 @@ export class StructuresController { if (!structure) { throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); } - user.pendingStructuresLink = [new Types.ObjectId(id)]; + user.structuresLink = [new Types.ObjectId(id)]; + // If user already exist, use created account if (await this.userService.verifyUserExist(user.email)) { this.tempUserService.sendUserMail(user as ITempUser, structure.structureName, true); @@ -225,24 +227,6 @@ 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<void> { - // Get structure name - const structure = await this.structureService.findOne(id); - if (!structure) { - throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); - } - // Get user and add structure to user - const userFromDb = await this.userService.findOne(user.email); - if (!userFromDb) { - throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); - } - await this.userService.updateStructureLinked(userFromDb.email, id); - // Send structure owners an email - this.structureService.sendStructureJoinRequest(userFromDb, structure); - } - @Delete(':id/owner/:userId') @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) @ApiParam({ name: 'id', type: String, required: true }) @@ -280,7 +264,7 @@ export class StructuresController { } // Get temp user const userFromDb = await this.tempUserService.findById(userId); - if (!userFromDb || !userFromDb.pendingStructuresLink.includes(new Types.ObjectId(id))) { + if (!userFromDb || !userFromDb.structuresLink.includes(new Types.ObjectId(id))) { throw new HttpException('Invalid temp user', HttpStatus.NOT_FOUND); } this.tempUserService.removeFromStructureLinked(userFromDb.email, id); diff --git a/src/structures/structures.module.ts b/src/structures/structures.module.ts index b15f1e65feb5220d43827a59a6835ac40ef3354b..4c4eedb5aa93f8b6391e4b132fc9f02bd13da26f 100644 --- a/src/structures/structures.module.ts +++ b/src/structures/structures.module.ts @@ -1,5 +1,6 @@ import { HttpModule } from '@nestjs/axios'; import { forwardRef, Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; import { MongooseModule } from '@nestjs/mongoose'; import { CategoriesModule } from '../categories/categories.module'; import { MailerModule } from '../mailer/mailer.module'; @@ -17,6 +18,8 @@ import { StructureTypeController } from './structure-type/structure-type.control import { StructureType, StructureTypeSchema } from './structure-type/structure-type.schema'; import { StructureTypeService } from './structure-type/structure-type.service'; import { StructuresController } from './structures.controller'; +import { config } from 'dotenv'; +config(); @Module({ imports: [ @@ -32,6 +35,10 @@ import { StructuresController } from './structures.controller'; CategoriesModule, TempUserModule, SearchModule, + JwtModule.register({ + secret: process.env.JWT_SECRET, + signOptions: { expiresIn: '30d' }, // 1 month validity + }), ], 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 index 94b61f14d79a02c1539e8dc775dd9391c0698263..49ced1f9bff130c0f659777e365f259aad5b9f50 100644 --- a/src/temp-user/dto/create-temp-user.dto.ts +++ b/src/temp-user/dto/create-temp-user.dto.ts @@ -10,5 +10,5 @@ export class CreateTempUserDto { @IsArray() @IsOptional() - pendingStructuresLink?: Types.ObjectId[]; + structuresLink?: Types.ObjectId[]; } diff --git a/src/temp-user/temp-user.interface.ts b/src/temp-user/temp-user.interface.ts index 2a4e74ff88ff5ec98d24b32178e84ac01bcb494b..67eb0e73400c84e3899ab8ab73fe571f91f68672 100644 --- a/src/temp-user/temp-user.interface.ts +++ b/src/temp-user/temp-user.interface.ts @@ -2,5 +2,5 @@ import { Document, Types } from 'mongoose'; export interface ITempUser extends Document { email: string; - pendingStructuresLink?: Types.ObjectId[]; + structuresLink?: Types.ObjectId[]; } diff --git a/src/temp-user/temp-user.schema.ts b/src/temp-user/temp-user.schema.ts index 3112b3112f6217e5f5ab30242257aece39726a51..4f8d266cded6d9cd2df5dfb431f10cc0c8bace48 100644 --- a/src/temp-user/temp-user.schema.ts +++ b/src/temp-user/temp-user.schema.ts @@ -9,7 +9,7 @@ export class TempUser { email: string; @Prop({ default: null }) - pendingStructuresLink?: Types.ObjectId[]; + structuresLink?: Types.ObjectId[]; } export const TempUserSchema = SchemaFactory.createForClass(TempUser); diff --git a/src/temp-user/temp-user.service.spec.ts b/src/temp-user/temp-user.service.spec.ts index 5e0efeb090b0dace5ed4cd7da1aad5724c18c0a7..141391ea3b2c78f0d2a0bdb9e08b880a60a0ffa3 100644 --- a/src/temp-user/temp-user.service.spec.ts +++ b/src/temp-user/temp-user.service.spec.ts @@ -88,14 +88,14 @@ describe('TempUserService', () => { describe('updateStructureLinked', () => { it('should update structure linked', async () => { - const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] }; + const tmpUser = { email: 'test2@test.com', structuresLink: [] }; tempUserModelMock.find.mockReturnThis(); tempUserModelMock.exec.mockResolvedValueOnce([]).mockResolvedValueOnce(tmpUser); tempUserModelMock.findByIdAndUpdate.mockReturnThis(); expect(await service.updateStructureLinked(tmpUser)).toEqual(tmpUser); }); it('should not update structure linked: User already linked', async () => { - const tmpUser = { email: 'test2@test.com', pendingStructuresLink: [] }; + const tmpUser = { email: 'test2@test.com', structuresLink: [] }; tempUserModelMock.find.mockReturnThis(); tempUserModelMock.findByIdAndUpdate.mockReturnThis(); tempUserModelMock.exec.mockResolvedValueOnce([tmpUser]); diff --git a/src/temp-user/temp-user.service.ts b/src/temp-user/temp-user.service.ts index fcf2a12319470a93ed03c2c32ad57f055fd5a820..80760cf9ff093ae65069a182267049a22d536c6b 100644 --- a/src/temp-user/temp-user.service.ts +++ b/src/temp-user/temp-user.service.ts @@ -1,4 +1,4 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import * as ejs from 'ejs'; import { Model, Types } from 'mongoose'; @@ -9,6 +9,8 @@ import { TempUser } from './temp-user.schema'; @Injectable() export class TempUserService { + private readonly logger = new Logger(TempUserService.name); + constructor( private readonly mailerService: MailerService, @InjectModel(TempUser.name) private tempUserModel: Model<ITempUser> @@ -20,6 +22,8 @@ export class TempUserService { throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); } const createUser = await this.tempUserModel.create(createTempUser); + + this.logger.debug(`TempUsersService | tempUser created`); // Send email this.sendUserMail(createUser, structureName); return this.findOne(createTempUser.email); @@ -50,18 +54,19 @@ export class TempUserService { email: createTempUser.email, }, { - pendingStructuresLink: { $in: [createTempUser.pendingStructuresLink[0]] }, + structuresLink: { $in: [createTempUser.structuresLink[0]] }, }, ], }) .exec(); + if (userInDb.length > 0) { throw new HttpException('User already linked', HttpStatus.UNPROCESSABLE_ENTITY); } return this.tempUserModel .findByIdAndUpdate( { email: createTempUser.email }, - { $push: { pendingStructuresLink: createTempUser.pendingStructuresLink[0] } } + { $push: { structuresLink: new Types.ObjectId(createTempUser.structuresLink[0]) } } ) .exec(); } @@ -89,23 +94,25 @@ export class TempUserService { public async getStructureTempUsers(structureId: string): Promise<ITempUser[]> { return this.tempUserModel - .find({ pendingStructuresLink: new Types.ObjectId(structureId) }) + .find({ structuresLink: new Types.ObjectId(structureId) }) .select('email updatedAt') .exec(); } public async removeFromStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { const user = await this.findOne(userEmail); + this.logger.debug(`find user : ${JSON.stringify(user)}`); + if (!user) { throw new HttpException('Invalid temp user', HttpStatus.NOT_FOUND); } - if (!user.pendingStructuresLink.includes(new Types.ObjectId(idStructure))) { + if (!user.structuresLink.includes(new Types.ObjectId(idStructure))) { throw new HttpException("Temp user doesn't belong to this structure", HttpStatus.NOT_FOUND); } - user.pendingStructuresLink = user.pendingStructuresLink.filter((structureId) => { + user.structuresLink = user.structuresLink.filter((structureId) => { return !structureId.equals(idStructure); }); await user.save(); - return user.pendingStructuresLink; + return user.structuresLink; } } diff --git a/src/users/controllers/users.controller.spec.ts b/src/users/controllers/users.controller.spec.ts index 7813b8fe1572eb102c15a118beb33226b75adab3..6760594ee110717b2df4a5786829109fbeb1cda7 100644 --- a/src/users/controllers/users.controller.spec.ts +++ b/src/users/controllers/users.controller.spec.ts @@ -1,5 +1,6 @@ import { HttpModule } from '@nestjs/axios'; import { HttpStatus } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import { Types } from 'mongoose'; @@ -69,6 +70,10 @@ describe('UsersController', () => { delete: jest.fn(), findOne: jest.fn(), }; + const mockJwtService = { + sign: jest.fn(), + decode: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -98,6 +103,10 @@ describe('UsersController', () => { provide: JobsService, useValue: jobServiceMock, }, + { + provide: JwtService, + useValue: mockJwtService, + }, { provide: getModelToken('TempUser'), useValue: TempUser, @@ -252,7 +261,6 @@ describe('UsersController', () => { const tempUserDeleteSpyer = jest.spyOn(tempUserServiceMock, 'delete'); const createUserDto = new CreateUserDto(); - createUserDto.pendingStructuresLink = []; userServiceMock.create.mockResolvedValueOnce(usersMockData[0]); const result = await controller.create(createUserDto); @@ -265,28 +273,6 @@ describe('UsersController', () => { expect(result).toEqual(usersMockData[0]); }); - it('should create user with structure', async () => { - const userCreateSpyer = jest.spyOn(userServiceMock, 'create'); - const structureFindOneSpyer = jest.spyOn(structureServiceMock, 'findOne'); - const updateStructureLinkedClaimSpyer = jest.spyOn(userServiceMock, 'updateStructureLinkedClaim'); - const sendAdminStructureNotificationSpyer = jest.spyOn(structureServiceMock, 'sendAdminStructureNotification'); - const tempUserFindOneSpyer = jest.spyOn(tempUserServiceMock, 'findOne'); - const tempUserDeleteSpyer = jest.spyOn(tempUserServiceMock, 'delete'); - - const createUserDto = new CreateUserDto(); - createUserDto.pendingStructuresLink = ['6093ba0e2ab5775cfc01fffe']; - userServiceMock.create.mockResolvedValueOnce(usersMockData[0]); - const result = await controller.create(createUserDto); - - expect(userCreateSpyer).toBeCalledTimes(1); - expect(structureFindOneSpyer).toBeCalledTimes(1); - expect(updateStructureLinkedClaimSpyer).toBeCalledTimes(1); - expect(sendAdminStructureNotificationSpyer).toBeCalledTimes(1); - expect(tempUserFindOneSpyer).toBeCalledTimes(1); - expect(tempUserDeleteSpyer).toBeCalledTimes(0); - expect(result).toEqual(usersMockData[0]); - }); - it('should create user with temp user', async () => { const userCreateSpyer = jest.spyOn(userServiceMock, 'create'); const structureFindOneSpyer = jest.spyOn(structureServiceMock, 'findOne'); @@ -296,7 +282,6 @@ describe('UsersController', () => { const tempUserDeleteSpyer = jest.spyOn(tempUserServiceMock, 'delete'); const createUserDto = new CreateUserDto(); - createUserDto.pendingStructuresLink = []; userServiceMock.create.mockResolvedValueOnce(usersMockData[0]); tempUserServiceMock.findOne.mockResolvedValueOnce({ email: 'test@test.com', pendingStructuresLink: [] }); const result = await controller.create(createUserDto); diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index 666ce7ddc0a9ba4f96dc829aaf28ecbb593b9970..d9663431640f7182789f3af877531cab1c50802f 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -23,7 +23,6 @@ import { PasswordResetDto } from '../dto/reset-password.dto'; import { UsersService } from '../services/users.service'; import { StructuresService } from '../../structures/services/structures.service'; import { TempUserService } from '../../temp-user/temp-user.service'; -import { ConfigurationService } from '../../configuration/configuration.service'; import { Structure } from '../../structures/schemas/structure.schema'; import { ProfileDto } from '../dto/profile.dto'; import { EmployerService } from '../services/employer.service'; @@ -32,6 +31,11 @@ import { IUser } from '../interfaces/user.interface'; import { UpdateDetailsDto } from '../dto/update-details.dto'; import { User } from '../schemas/user.schema'; import { DescriptionDto } from '../dto/description.dto'; +import { Types } from 'mongoose'; +import { IPendingStructureToken } from '../interfaces/pending-structure-token.interface'; +import { DateTime } from 'luxon'; +import { JwtService } from '@nestjs/jwt'; + @ApiTags('users') @Controller('users') export class UsersController { @@ -39,10 +43,10 @@ export class UsersController { constructor( private usersService: UsersService, private structureService: StructuresService, - private tempUserService: TempUserService, + private tempusersService: TempUserService, private employerService: EmployerService, private jobsService: JobsService, - private configurationService: ConfigurationService + private jwtService: JwtService ) {} @UseGuards(JwtAuthGuard) @@ -95,27 +99,11 @@ export class UsersController { @ApiResponse({ status: 201, description: 'User created' }) public async create(@Body() createUserDto: CreateUserDto) { this.logger.debug('create'); - // remove structureId for creation and add structure after - let structureId = null; - if (createUserDto.pendingStructuresLink.length > 0) { - structureId = createUserDto.pendingStructuresLink[0]; - delete createUserDto.pendingStructuresLink; - } const user = await this.usersService.create(createUserDto); - if (structureId) { - const structure = await this.structureService.findOne(structureId); - this.usersService.updateStructureLinkedClaim(createUserDto.email, structureId, structure); - this.structureService.sendAdminStructureNotification( - null, - this.configurationService.config.templates.adminStructureClaim.ejs, - this.configurationService.config.templates.adminStructureClaim.json, - user - ); - } // Remove temp user if exist - const tempUser = await this.tempUserService.findOne(createUserDto.email); + const tempUser = await this.tempusersService.findOne(createUserDto.email); if (tempUser) { - this.tempUserService.delete(createUserDto.email); + this.tempusersService.delete(createUserDto.email); } return user; } @@ -216,4 +204,101 @@ export class UsersController { public async updateDescription(@Req() req, @Body() body: DescriptionDto): Promise<User> { return this.usersService.updateDescription(req.user._id, body); } + + @Post('join-request/:id') + @ApiParam({ name: 'id', type: String, required: true }) + public async join(@Param('id') id: string, @Body() user: User): Promise<void> { + // Get structure name + const structure = await this.structureService.findOne(id); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + // Get user and add structure to user + const userFromDb = await this.usersService.findOne(user.email); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + // See structure to pending + const pendingStructures = await this.usersService.updatePendingStructureLinked( + userFromDb.email, + id, + structure.structureName + ); + const token = pendingStructures + .filter((pending) => pending.id.equals(structure.id)) + .map((pending) => pending.token); + + // await this.usersService.updateStructureLinked(userFromDb.email, id); + // Send structure owners an email + this.structureService.sendStructureJoinRequest(userFromDb, structure, token[0]); + } + + //add token in route + add infos in token + @Get('join-validate/:token/:status') + @ApiParam({ name: 'token', type: String, required: true }) + @ApiParam({ name: 'status', type: String, required: true }) + public async joinValidation(@Param('token') token: string, @Param('status') status: string): Promise<any> { + const decoded: IPendingStructureToken = this.jwtService.decode(token) as IPendingStructureToken; + const today = DateTime.local().setZone('utc', { keepLocalTime: true }); + + if (!token || !status) { + throw new HttpException('Wrong parameters', HttpStatus.NOT_FOUND); + } + if (decoded.expiresAt < today) { + throw new HttpException('Expired or invalid token', HttpStatus.FORBIDDEN); + } + // Get structure name + const structure = await this.structureService.findOne(decoded.idStructure); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + // Get user and add pending structure + const userFromDb = await this.usersService.findById(decoded.userId); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + if ( + !userFromDb.pendingStructuresLink + .map((pending) => pending.id) + .filter((id) => id.equals(new Types.ObjectId(decoded.idStructure))) + ) { + throw new HttpException('User not linked to structure', HttpStatus.NOT_FOUND); + } + if (status == 'true') { + // Accept + await this.usersService.updateStructureLinked(userFromDb.email, decoded.idStructure); + await this.usersService.removeFromPendingStructureLinked(userFromDb.email, decoded.idStructure); + } else { + // Refuse + this.usersService.removeFromPendingStructureLinked(userFromDb.email, decoded.idStructure); + } + await this.usersService.sendStructureClaimApproval(userFromDb.email, structure.structureName, status == 'true'); + + return { id: decoded.idStructure, name: structure.structureName }; + } + + // Cancel a user's join request + @Get('join-cancel/:idStructure/:idUser') + @ApiParam({ name: 'idStructure', type: String, required: true }) + @ApiParam({ name: 'idUser', type: String, required: true }) + public async joinCancel(@Param('idStructure') idStructure: string, @Param('idUser') idUser: string): Promise<any> { + // Get structure name + const structure = await this.structureService.findOne(idStructure); + if (!structure) { + throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND); + } + // Get user and add pending structure + const userFromDb = await this.usersService.findById(idUser); + if (!userFromDb) { + throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); + } + if ( + !userFromDb.pendingStructuresLink + .map((pending) => pending.id) + .filter((id) => id.equals(new Types.ObjectId(idStructure))) + ) { + throw new HttpException('This structure is in pending state', HttpStatus.NOT_FOUND); + } + await this.usersService.removeFromPendingStructureLinked(userFromDb.email, idStructure); + } } diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index f304639c223b739d958b4648972beccb4115e27d..6c24b853abd3db1296ab361232247bbae8a51006 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsArray, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { Types } from 'mongoose'; import { PersonalOffer } from '../../personal-offers/schemas/personal-offer.schema'; export class CreateUserDto { @@ -27,11 +28,7 @@ export class CreateUserDto { @IsArray() @IsOptional() - pendingStructuresLink?: Array<string>; - - @IsArray() - @IsOptional() - structuresLink?: Array<string>; + structuresLink?: Array<Types.ObjectId>; @IsOptional() unattachedSince?: Date; diff --git a/src/users/interfaces/pending-structure-token.interface.ts b/src/users/interfaces/pending-structure-token.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..76b1e762f0cf1446f41d9d89f3130c46777c4bfa --- /dev/null +++ b/src/users/interfaces/pending-structure-token.interface.ts @@ -0,0 +1,5 @@ +export interface IPendingStructureToken { + userId: string; + idStructure: string; + expiresAt: string; +} diff --git a/src/users/interfaces/pendingStructure.ts b/src/users/interfaces/pendingStructure.ts new file mode 100644 index 0000000000000000000000000000000000000000..addfcbae1ab9ef7e9fb065aac2fc603ac76ae906 --- /dev/null +++ b/src/users/interfaces/pendingStructure.ts @@ -0,0 +1,8 @@ +import { Types } from 'mongoose'; + +export interface pendingStructuresLink { + id: Types.ObjectId; + structureName: string; + token: string; + createdAt: string; +} diff --git a/src/users/schemas/user.schema.ts b/src/users/schemas/user.schema.ts index 08c9213fabd19d63ee418f347b7fe6df75d5ab0b..bcb597c034f04aa2389e8b7641ec5ae941d82223 100644 --- a/src/users/schemas/user.schema.ts +++ b/src/users/schemas/user.schema.ts @@ -4,6 +4,7 @@ import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-of import { Employer } from './employer.schema'; import { Job } from './job.schema'; import { UserRole } from '../enum/user-role.enum'; +import { pendingStructuresLink } from '../interfaces/pendingStructure'; @Schema({ timestamps: true }) export class User { @@ -47,7 +48,7 @@ export class User { structuresLink: Types.ObjectId[]; @Prop({ default: null }) - pendingStructuresLink: Types.ObjectId[]; + pendingStructuresLink: pendingStructuresLink[]; @Prop({ default: null }) structureOutdatedMailSent: Types.ObjectId[]; diff --git a/src/users/services/users.service.spec.ts b/src/users/services/users.service.spec.ts index 511d88060451f916d5fb3d8fd45641c90750aa25..3ca3c313b64fc18c815e858c7397be264e871a28 100644 --- a/src/users/services/users.service.spec.ts +++ b/src/users/services/users.service.spec.ts @@ -1,5 +1,6 @@ import { HttpModule } from '@nestjs/axios'; import { HttpException, HttpStatus } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import * as bcrypt from 'bcrypt'; @@ -72,13 +73,18 @@ const mockUserRegistrySearchService = { update: jest.fn(), }; +const mockJwtService = { + sign: jest.fn(), + decode: jest.fn(), +}; + const createUserDto: CreateUserDto = { email: 'jacques.dupont@mii.com', password: 'test1A!!', name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', - structuresLink: ['61e9260c2ac971550065e262', '61e9260b2ac971550065e261'], + structuresLink: [new Types.ObjectId('61e9260c2ac971550065e262'), new Types.ObjectId('61e9260b2ac971550065e261')], }; const mockUser: User = { @@ -124,6 +130,10 @@ describe('UsersService', () => { provide: UserRegistrySearchService, useValue: mockUserRegistrySearchService, }, + { + provide: JwtService, + useValue: mockJwtService, + }, { provide: getModelToken('User'), useValue: mockUserModel, diff --git a/src/users/services/users.service.ts b/src/users/services/users.service.ts index 334f88696375d41b46b5a8860ad7fc7c96a07dd2..a576cee66fb9e42aa35bee67b403d4e82ad5840a 100644 --- a/src/users/services/users.service.ts +++ b/src/users/services/users.service.ts @@ -1,5 +1,7 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { InjectModel } from '@nestjs/mongoose'; +import { Cron, CronExpression } from '@nestjs/schedule'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; import * as ejs from 'ejs'; @@ -16,6 +18,7 @@ import { EmailChangeDto } from '../dto/change-email.dto'; import { CreateUserDto } from '../dto/create-user.dto'; import { DescriptionDto } from '../dto/description.dto'; import { UpdateDetailsDto } from '../dto/update-details.dto'; +import { pendingStructuresLink } from '../interfaces/pendingStructure'; import { IUser } from '../interfaces/user.interface'; import { IUserRegistry } from '../interfaces/userRegistry.interface'; import { EmployerDocument } from '../schemas/employer.schema'; @@ -30,7 +33,8 @@ export class UsersService { @InjectModel(User.name) private userModel: Model<IUser>, private readonly mailerService: MailerService, private userRegistrySearchService: UserRegistrySearchService, - private configurationService: ConfigurationService + private configurationService: ConfigurationService, + private jwtService: JwtService ) {} /** @@ -49,12 +53,7 @@ export class UsersService { ); } let createUser = new this.userModel(createUserDto); - createUser.structuresLink = []; - if (createUserDto.structuresLink) { - createUserDto.structuresLink.forEach((structureId) => { - createUser.structuresLink.push(new Types.ObjectId(structureId)); - }); - } + createUser.structuresLink = createUser.structuresLink.map((id) => new Types.ObjectId(id)); createUser.password = await this.hashPassword(createUser.password); createUser.unattachedSince = DateTime.local(); // Send verification email @@ -257,7 +256,7 @@ export class UsersService { * a new account. * @param user User */ - private async sendStructureClaimApproval(userEmail: string, structureName: string, status: boolean): Promise<any> { + public async sendStructureClaimApproval(userEmail: string, structureName: string, status: boolean): Promise<any> { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureClaimValidation.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureClaimValidation.json); @@ -462,7 +461,7 @@ export class UsersService { public async isUserAlreadyClaimedStructure(structureId: string, userEmail: string): Promise<boolean> { const user = await this.findOne(userEmail, true); if (user) { - return user.pendingStructuresLink.includes(new Types.ObjectId(structureId)); + return user.pendingStructuresLink.map((pending) => pending.id).includes(new Types.ObjectId(structureId)); } return false; } @@ -496,18 +495,49 @@ export class UsersService { userEmail: string, idStructure: string, structure: StructureDocument - ): Promise<Types.ObjectId[]> { - const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure); + ): Promise<pendingStructuresLink[]> { + const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure, structure.structureName); this.sendAdminStructureValidationMail(userEmail, structure); return stucturesLinked; } - public async updatePendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + /** + * Creates a 1 month valid token for a pending structure request + * @param user + * @param idStructure + * @returns + */ + private _createPendingToken(user: IUser, idStructure: string): { token: string; createdAt: string } { + const local = DateTime.local().setZone('Europe/Paris'); + return { + token: this.jwtService.sign({ userId: user.id, idStructure: idStructure, expiresAt: local.plus({ month: 1 }) }), + createdAt: local, + }; + } + + /** + * Updates the array of user's pending structures + * @param userEmail + * @param idStructure + * @returns + */ + public async updatePendingStructureLinked( + userEmail: string, + idStructure: string, + structureName: string + ): Promise<pendingStructuresLink[]> { const user = await this.findOne(userEmail, true); if (user) { - if (!user.pendingStructuresLink.includes(new Types.ObjectId(idStructure))) { - user.pendingStructuresLink.push(new Types.ObjectId(idStructure)); + if (!user.pendingStructuresLink.map((pending) => pending.id).includes(new Types.ObjectId(idStructure))) { + const { token, createdAt } = this._createPendingToken(user, idStructure); + + user.pendingStructuresLink.push({ + id: new Types.ObjectId(idStructure), + token: token, + createdAt: createdAt, + structureName: structureName, + }); await user.save(); return user.pendingStructuresLink; } @@ -516,17 +546,30 @@ export class UsersService { throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); } - public async removeFromPendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> { + /** + * Removes a strcture from the users's pending list + * @param userEmail + * @param idStructure + * @returns + */ + public async removeFromPendingStructureLinked( + userEmail: string, + idStructure: string + ): Promise<pendingStructuresLink[]> { const user = await this.findOne(userEmail, true); if (user) { - if (user.pendingStructuresLink.includes(new Types.ObjectId(idStructure))) { - user.pendingStructuresLink = user.pendingStructuresLink.filter((structureId) => { - return structureId === new Types.ObjectId(idStructure); + if ( + user.pendingStructuresLink + .map((pending) => pending.id) + .filter((id) => id.equals(new Types.ObjectId(idStructure))) + ) { + user.pendingStructuresLink = user.pendingStructuresLink.filter((pending) => { + return !pending.id.equals(new Types.ObjectId(idStructure)); }); await user.save(); return user.pendingStructuresLink; } - throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND); + throw new HttpException('User already belong to this structure', HttpStatus.UNPROCESSABLE_ENTITY); } throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); } @@ -540,7 +583,7 @@ export class UsersService { await user.save(); return user.structuresLink; } - throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND); + throw new HttpException('User already belong to this structure', HttpStatus.UNPROCESSABLE_ENTITY); } throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); } @@ -565,19 +608,55 @@ export class UsersService { /** * Return all pending attachments of all profiles */ - public async getPendingStructures(): Promise<PendingStructureDto[]> { - const users = await this.userModel.find(); - const structuresPending = []; - - // For each user, if they have structures in pending, push them in tab and return this tab. - users.forEach((user) => { - if (user.pendingStructuresLink.length) { - user.pendingStructuresLink.forEach((structureId) => { - structuresPending.push({ userEmail: user.email, structureId: structureId }); - }); + public async getPendingStructures(returnUsers = false): Promise<PendingStructureDto[] | IUser[]> { + const users = await this.userModel.find({ 'pendingStructuresLink.0': { $exists: true } }).exec(); + if (!users.length) { + return []; + } + if (returnUsers) { + return users; + } else { + let structuresPending = []; + for (const user of users) { + structuresPending = structuresPending.concat( + this.parsePendingStructureLinkToStructureDto(user.email, user.pendingStructuresLink) + ); } - }); - return structuresPending; + return structuresPending as PendingStructureDto[]; + } + } + /** + * Return all pending attachments of all profiles + */ + // public async getUsersWithExpiredPendingStructuresDemands(): Promise<User[]> { + // const users = await this.userModel.find({ 'pendingStructuresLink.0': { $exists: true } }).exec().then((user) => { + + // }); + // if (!users.length) { + // return []; + // } + + // return users; + // } + + /** + * parsePendingStructureLinkToStructureDto + */ + public parsePendingStructureLinkToStructureDto( + userEmail: string, + pendingStructureLink: pendingStructuresLink[] + ): PendingStructureDto[] { + const pendingStructureDto: PendingStructureDto[] = []; + for (const pendingLink of pendingStructureLink) { + const strDto: PendingStructureDto = { + userEmail: userEmail, + structureId: pendingLink.id, + createdAt: new Date(pendingLink.createdAt), + structureName: pendingLink.structureName, + }; + pendingStructureDto.push(strDto); + } + return pendingStructureDto; } /** @@ -591,17 +670,21 @@ export class UsersService { ): Promise<PendingStructureDto[]> { const user = await this.findOne(userEmail); // Get other users who have made the demand on the same structure + const otherUsers = await this.userModel - .find({ pendingStructuresLink: new Types.ObjectId(structureId), email: { $ne: userEmail } }) + .find({ 'pendingStructuresLink.id': { $eq: new Types.ObjectId(structureId) }, email: { $ne: userEmail } }) .exec(); let status = false; if (!user) { throw new HttpException('User not found', HttpStatus.NOT_FOUND); } - if (user.pendingStructuresLink.includes(new Types.ObjectId(structureId))) { + if ( + user.pendingStructuresLink.map((pending) => pending.id).filter((id) => id.equals(new Types.ObjectId(structureId))) + .length + ) { user.pendingStructuresLink = user.pendingStructuresLink.filter((item) => { - return !new Types.ObjectId(structureId).equals(item); + return !new Types.ObjectId(structureId).equals(item.id); }); // If it's a validation case, push structureId into validated user structures if (validate) { @@ -613,7 +696,7 @@ export class UsersService { otherUsers.forEach((otherUser) => { // Remove the structure id from their demand otherUser.pendingStructuresLink = otherUser.pendingStructuresLink.filter((item) => { - return !new Types.ObjectId(structureId).equals(item); + return !new Types.ObjectId(structureId).equals(item.id); }); // Send a rejection email this.sendStructureClaimApproval(otherUser.email, structureName, false); @@ -623,7 +706,7 @@ export class UsersService { } this.sendStructureClaimApproval(userEmail, structureName, status); await user.save(); - return this.getPendingStructures(); + return (await this.getPendingStructures()) as PendingStructureDto[]; } else { throw new HttpException( 'Cannot validate strucutre. It might have been already validate, or the structure doesn`t belong to the user', @@ -812,4 +895,21 @@ export class UsersService { .populate('personalOffers') .exec(); } + + @Cron(CronExpression.EVERY_DAY_AT_3AM) + public async cleanPendingStructures(): Promise<void> { + this.logger.debug('pendingStructuresCleaning process'); + const users: IUser[] = (await this.getPendingStructures(true)) as IUser[]; + for (const user of users) { + for (const [index, pending] of user.pendingStructuresLink.entries()) { + try { + this.jwtService.verify(pending.token); + } catch (e) { + this.logger.debug(`Expired token for structure ${pending.structureName}`); + user.pendingStructuresLink.splice(index, 1); + this.userModel.findByIdAndUpdate({ _id: user.id }, user).exec(); + } + } + } + } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index d1e46c0c00ae4f03723f51bb17ef2e25f8d663ce..4a13ec47bee12c73e686a555db4bc2fab1794432 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -18,6 +18,9 @@ import { SearchModule } from '../search/search.module'; import { UsersRegistryController } from './controllers/userRegistry.controller'; import { UserRegistryService } from './services/userRegistry.service'; import { UserRegistrySearchService } from './services/userRegistry-search.service'; +import { JwtModule } from '@nestjs/jwt'; +import { config } from 'dotenv'; +config(); @Module({ imports: [ @@ -31,6 +34,10 @@ import { UserRegistrySearchService } from './services/userRegistry-search.servic HttpModule, TempUserModule, SearchModule, + JwtModule.register({ + secret: process.env.JWT_SECRET, + signOptions: { expiresIn: '30d' }, // 1 month validity + }), ], providers: [ UsersService, diff --git a/test/mock/services/structures.mock.service.ts b/test/mock/services/structures.mock.service.ts index d93d40d6c623231537dde474bfcf2b5fa6fbbcfe..ee89e8a9f703005a0ecfa2ee3d3479183feea0f7 100644 --- a/test/mock/services/structures.mock.service.ts +++ b/test/mock/services/structures.mock.service.ts @@ -1,11 +1,12 @@ import { HttpException, HttpStatus } from '@nestjs/common'; +import { Types } from 'mongoose'; import { PersonalOfferDocument } from '../../../src/personal-offers/schemas/personal-offer.schema'; import { CNFSStructure } from '../../../src/structures/interfaces/cnfs-structure.interface'; import { Structure, StructureDocument } from '../../../src/structures/schemas/structure.schema'; export class StructuresServiceMock { - findOne(id) { - if (id === '6093ba0e2ab5775cfc01ed3e') { + findOne(id: Types.ObjectId) { + if (id.toString() === '6093ba0e2ab5775cfc01ed3e') { return { _id: '6093ba0e2ab5775cfc01ed3e', coord: [4.8498155, 45.7514817], @@ -109,7 +110,7 @@ export class StructuresServiceMock { }; } - if (id === '6903ba0e2ab5775cfc01ed4d') { + if (id.toString() === '6903ba0e2ab5775cfc01ed4d') { return { _id: '6903ba0e2ab5775cfc01ed4d', structureType: null, diff --git a/test/mock/services/user.mock.service.ts b/test/mock/services/user.mock.service.ts index 629b5e6d1b1145a2d9fd56f5108c47a431e2ec47..371097c787aeaec791ff41a5a3f8239a40fbeff7 100644 --- a/test/mock/services/user.mock.service.ts +++ b/test/mock/services/user.mock.service.ts @@ -117,17 +117,17 @@ export class UsersServiceMock { }; } - getPendingStructures() { + getPendingStructures(): PendingStructureDto[] { return [ { - structureId: '6093ba0e2ab5775cfc01ed3e', + structureId: new Types.ObjectId('6093ba0e2ab5775cfc01ed3e'), structureName: 'a', userEmail: 'paula.dubois@mii.com', createdAt: new Date('2021-02-02T10:07:48.000Z'), updatedAt: new Date('2021-03-02T10:07:48.000Z'), }, { - structureId: '6903ba0e2ab5775cfc01ed4d', + structureId: new Types.ObjectId('6903ba0e2ab5775cfc01ed4d'), structureName: "L'Atelier Numérique", userEmail: 'jacques.dupont@mii.com', createdAt: new Date('2021-02-02T10:07:48.000Z'),