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 } from 'mongoose'; import { LoginDto } from '../auth/login-dto'; import { CreateUserDto } from './create-user.dto'; import { User } from './user.schema'; import { MailerService } from '../mailer/mailer.service'; import { IUser } from './user.interface'; @Injectable() export class UsersService { constructor(@InjectModel(User.name) private userModel: Model<IUser>, private mailerService: MailerService) {} /** * Create a user account * @param createUserDto CreateUserDto */ public async create(createUserDto: CreateUserDto): Promise<User> { 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.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,})'); 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 findById(id: string, passwordQuery?: boolean): Promise<IUser | undefined> { if (passwordQuery) { return this.userModel.findById(id).exec(); } return this.userModel.findById(id).select('-password').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; } /** * 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<any> { const user = await this.findById(userId); if (user && user.validationToken === token) { user.validationToken = null; user.emailVerified = true; user.save(); } 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(); } }