import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import * as bcrypt from 'bcrypt'; import * as ejs from 'ejs'; import * as crypto from 'crypto'; import { Model, Types } from 'mongoose'; import { LoginDto } from '../auth/login-dto'; import { CreateUserDto } from './dto/create-user.dto'; import { User } from './schemas/user.schema'; import { MailerService } from '../mailer/mailer.service'; import { IUser } from './interfaces/user.interface'; import { EmailChangeDto } from './dto/change-email.dto'; import { PendingStructureDto } from '../admin/dto/pending-structure.dto'; import { OwnerDto } from './dto/owner.dto'; @Injectable() export class UsersService { constructor(@InjectModel(User.name) private userModel: Model<IUser>, private readonly mailerService: MailerService) {} /** * Create a user account * @param createUserDto CreateUserDto */ public async create(createUserDto: CreateUserDto): Promise<User | HttpStatus> { const userInDb = await this.findOne(createUserDto.email); if (userInDb) { throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); } if (!this.isStrongPassword(createUserDto.password)) { throw new HttpException( 'Weak password, it must contain ne lowercase alphabetical character, one uppercase alphabetical character, one numeric character, one special character and be eight characters or longer', HttpStatus.UNPROCESSABLE_ENTITY ); } let createUser = new this.userModel(createUserDto); createUser.structuresLink = []; if (createUserDto.structuresLink) { createUserDto.structuresLink.forEach((structureId) => { createUser.structuresLink.push(Types.ObjectId(structureId)); }); } // createUser.email = createUserDto.email; createUser.password = await this.hashPassword(createUser.password); // Send verification email createUser = await this.verifyUserMail(createUser); createUser.save(); return await this.findOne(createUserDto.email); } /** * Verify password strenth with the following rule: * - The string must contain at least 1 lowercase alphabetical character * - The string must contain at least 1 uppercase alphabetical character * - The string must contain at least 1 numeric character * - The string must contain at least one special character, reserved RegEx characters are escaped to avoid conflict * - The string must be eight characters or longer * @param password string */ private isStrongPassword(password: string): boolean { const strongRegex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})'); //NOSONAR return strongRegex.test(password); } private async comparePassword(attempt: string, password: string): Promise<boolean> { return await bcrypt.compare(attempt, password); } private async hashPassword(password: string): Promise<string> { return await bcrypt.hash(password, process.env.SALT); } public async findOne(mail: string, passwordQuery?: boolean): Promise<IUser | undefined> { if (passwordQuery) { return this.userModel.findOne({ email: mail }).exec(); } return this.userModel.findOne({ email: mail }).select('-password').exec(); } public async findAll(): Promise<User[]> { return await this.userModel.find().exec(); } public async findById(id: string, passwordQuery?: boolean): Promise<IUser | undefined> { if (passwordQuery) { return this.userModel.findById(id).exec(); } return this.userModel.findById(id).select('-password').exec(); } public async removeStructureIdFromUsers(structureId: Types.ObjectId): Promise<IUser[] | undefined> { return this.userModel .updateMany({ structuresLink: { $in: [structureId] } }, { $pull: { structuresLink: structureId } }) .exec(); } /** * Return a user after credential checking. * Use for login action * @param param LoginDto */ public async findByLogin({ email, password }: LoginDto): Promise<User> { const user = await this.findOne(email, true); if (!user) { throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); } // compare passwords const areEqual = await this.comparePassword(password, user.password); if (!areEqual) { throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); } return user; } /** * Generate activation token and send it to user by email, in order to validate * a new account. * @param user User */ private async verifyUserMail(user: IUser): Promise<any> { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.verify.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.verify.json); const token = crypto.randomBytes(64).toString('hex'); const html = await ejs.renderFile(ejsPath, { config, token: token, userId: user._id, }); this.mailerService.send(user.email, jsonConfig.subject, html); // Save token user.validationToken = token; return user; } /** * Send to all admins mail for aptic duplicated data */ public async sendAdminApticStructureMail(structureName: string, duplicatedStructureName: string): Promise<any> { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.apticStructureDuplication.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.apticStructureDuplication.json); const html = await ejs.renderFile(ejsPath, { config, name: structureName, duplicatedStructureName: duplicatedStructureName, }); const admins = await this.getAdmins(); admins.forEach((admin) => { this.mailerService.send(admin.email, jsonConfig.subject, html); }); } /** * Send approval email for user * a new account. * @param user User */ private 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); const html = await ejs.renderFile(ejsPath, { config, status: status ? 'accepté' : 'refusée', name: structureName, }); this.mailerService.send(userEmail, jsonConfig.subject, html); } /** * Check that the given token is associated to userId. If it's true, validate user account. * @param userId string * @param token string */ public async validateUser(userId: string, token: string): Promise<User | HttpException> { const user = await this.findById(userId); if (user && user.validationToken === token) { user.validationToken = null; user.emailVerified = true; user.save(); return user; } else { throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); } } public async changeUserEmail(emailDto: EmailChangeDto): Promise<any> { const user = await this.findOne(emailDto.oldEmail); const alreadyUsed = await this.findOne(emailDto.newEmail); if (user) { if (!alreadyUsed) { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.changeEmail.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.changeEmail.json); const token = crypto.randomBytes(64).toString('hex'); const html = await ejs.renderFile(ejsPath, { config, token: token, }); this.mailerService.send(user.email, jsonConfig.subject, html); user.changeEmailToken = token; user.newEmail = emailDto.newEmail; user.save(); return user; } throw new HttpException('Email already used', HttpStatus.NOT_ACCEPTABLE); } throw new HttpException('Email sent if account exist', HttpStatus.UNAUTHORIZED); } public async verifyAndUpdateUserEmail(token: string): Promise<any> { const user = await this.userModel.findOne({ changeEmailToken: token }).exec(); if (user) { user.email = user.newEmail; user.newEmail = null; user.changeEmailToken = null; user.save(); return user; } else { throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); } } public async changeUserPassword(userId: string, oldPassword: string, newPassword: string): Promise<any> { const user = await this.findById(userId, true); const arePasswordEqual = await this.comparePassword(oldPassword, user.password); if (!arePasswordEqual) { throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); } if (!this.isStrongPassword(newPassword)) { throw new HttpException( 'Weak password, it must contain ne lowercase alphabetical character, one uppercase alphabetical character, one numeric character, one special character and be eight characters or longer', HttpStatus.UNPROCESSABLE_ENTITY ); } user.password = await this.hashPassword(newPassword); user.save(); } /** * Send reset password email based on ejs template * @param email string */ public async sendResetPasswordEmail(email: string): Promise<HttpException> { const user = await this.findOne(email); if (user) { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.resetPassword.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.resetPassword.json); const token = crypto.randomBytes(64).toString('hex'); const html = await ejs.renderFile(ejsPath, { config, token: token, }); this.mailerService.send(user.email, jsonConfig.subject, html); // Save token user.resetPasswordToken = token; user.save(); } throw new HttpException('Email sent if account exist', HttpStatus.OK); } /** * Change password with the given token and password * Token existence and password strength are verified * @param password string * @param token string */ public async validatePasswordResetToken(password: string, token: string): Promise<HttpException> { const user = await this.userModel.findOne({ resetPasswordToken: token }).exec(); if (user) { if (!this.isStrongPassword(password)) { throw new HttpException( 'Weak password, it must contain ne lowercase alphabetical character, one uppercase alphabetical character, one numeric character, one special character and be eight characters or longer', HttpStatus.UNPROCESSABLE_ENTITY ); } user.password = await this.hashPassword(password); user.resetPasswordToken = null; user.save(); throw new HttpException('Password Reset', HttpStatus.OK); } throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); } public async getAdmins(): Promise<User[]> { return this.userModel.find({ role: 1 }).exec(); } public async isStructureClaimed(structureId: string): Promise<IUser> { 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) { return user.pendingStructuresLink.includes(Types.ObjectId(structureId)); } return false; } /** * Send to all admins validation email for structures * new account. */ private async sendAdminStructureValidationMail(): Promise<any> { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.adminStructureClaim.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.adminStructureClaim.json); const html = await ejs.renderFile(ejsPath, { config, }); const admins = await this.getAdmins(); admins.forEach((admin) => { this.mailerService.send(admin.email, jsonConfig.subject, html); }); } 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)); await user.save(); return user.pendingStructuresLink; } throw new HttpException('User already claimed this structure', HttpStatus.NOT_FOUND); } 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.structuresLink.includes(Types.ObjectId(idStructure))) { user.structuresLink.push(Types.ObjectId(idStructure)); 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); } throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); } /** * 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 }); }); } }); return structuresPending; } /** * Validate or refuse a pending structure given a email and structure id */ public async validatePendingStructure( userEmail: string, structureId: string, structureName: string, validate: boolean ): 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: 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(Types.ObjectId(structureId))) { user.pendingStructuresLink = user.pendingStructuresLink.filter((item) => { return !Types.ObjectId(structureId).equals(item); }); // If it's a validation case, push structureId into validated user structures if (validate) { user.structuresLink.push(Types.ObjectId(structureId)); // Send validation email status = true; // For other users who have made the demand on the same structure if (otherUsers) { otherUsers.forEach((otherUser) => { // Remove the structure id from their demand otherUser.pendingStructuresLink = otherUser.pendingStructuresLink.filter((item) => { return !Types.ObjectId(structureId).equals(item); }); // Send a rejection email this.sendStructureClaimApproval(otherUser.email, structureName, false); otherUser.save(); }); } } this.sendStructureClaimApproval(userEmail, structureName, status); await user.save(); return this.getPendingStructures(); } else { throw new HttpException( 'Cannot validate strucutre. It might have been already validate, or the structure does`nt belong to the user', HttpStatus.NOT_FOUND ); } } public async removeOutdatedStructureFromArray(structureId: string): Promise<void> { const users = await this.userModel.find({ structureOutdatedMailSent: Types.ObjectId(structureId) }).exec(); users.forEach((user) => { user.structureOutdatedMailSent = user.structureOutdatedMailSent.filter((item) => Types.ObjectId(structureId).equals(item) ); user.save(); }); } public async verifyUserExist(email: string): Promise<boolean> { const user = await this.findOne(email); return user ? true : false; } public async deleteOne(email: string): Promise<User> { const user = await this.findOne(email); if (!user) { throw new HttpException('Invalid user email', HttpStatus.BAD_REQUEST); } return user.deleteOne(); } public async deleteOneId(id: string): Promise<User> { const user = await this.userModel.findOne({ _id: id }).exec(); if (!user) { throw new HttpException('Invalid user id', HttpStatus.BAD_REQUEST); } return user.deleteOne(); } public async getStructureOwnersMails(structureId: string, emailUser: string): Promise<OwnerDto[]> { const users = await this.userModel .find({ structuresLink: Types.ObjectId(structureId), email: { $ne: emailUser } }) .exec(); const owners: OwnerDto[] = []; users.forEach((user) => { const userProfile = new OwnerDto(); userProfile.email = user.email; userProfile.id = user._id; owners.push(userProfile); }); return owners; } public async searchUsers(searchString: string) { return this.userModel.find({ email: new RegExp(searchString, 'i') }).exec(); } }