Newer
Older
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);
}

Hugo SUBTIL
committed
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.password = await this.hashPassword(createUser.password);
// Send verification email
createUser = await this.verifyUserMail(createUser);

Hugo SUBTIL
committed
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

Hugo SUBTIL
committed
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> {

Hugo SUBTIL
committed
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,
});
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',
});
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();
} 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) {
Jérémie BRISON
committed
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;
Jérémie BRISON
committed
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();
}
Jérémie BRISON
committed
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));
Jérémie BRISON
committed
}
return false;
}

Hugo SUBTIL
committed
/**
* 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);

Hugo SUBTIL
committed
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,
validate: boolean
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 } })
let status = false;
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) {
// 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);
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);
});
public async searchUsers(searchString: string) {
return this.userModel.find({ email: new RegExp(searchString, 'i') }).exec();