import { Inject, Injectable } from '@nestjs/common'; import { ILike, Repository } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { UserTokenType } from '../user-token/user-token.entity'; import { User } from './user.entity'; import { TokenService } from 'src/modules/utility/services/token.service'; import { EmailService } from '../email/email.service'; import { RegistrationEmail } from './email/registration.email'; import { ForgotPasswordEmail } from './email/forgot-password.email'; import { UserTokenService } from '../user-token/user-token.service'; import { ConfigurationService } from 'src/modules/config/config.service'; import { Upload } from '../upload/upload.entity'; import { UploadService } from '../upload/upload.service'; @Injectable() export class UserService { constructor( @Inject('USER_REPOSITORY') private userRepository: Repository, private userToken: UserTokenService, private token: TokenService, private email: EmailService, private config: ConfigurationService, private upload: UploadService, ) {} public async getById(id: number, relations?: string[]): Promise { if (!id) { return null; } return this.userRepository.findOne({ where: { id }, relations }); } public async getByUUID(uuid: string, relations?: string[]): Promise { if (!uuid) { return null; } return this.userRepository.findOne({ where: { uuid }, relations }); } public async getByEmail(email: string, relations?: string[]): Promise { if (!email) { return null; } return this.userRepository.findOne({ where: { email }, relations }); } public async searchUsers( limit = 50, offset = 0, search?: string, relations?: string[], ): Promise<[User[], number]> { const searchTerm = `%${search}%`; return this.userRepository.findAndCount({ where: search ? [ { display_name: ILike(searchTerm), }, { username: ILike(searchTerm), }, { email: ILike(searchTerm), }, ] : undefined, skip: offset, take: limit, relations, }); } public async searchUsersCount( search?: string, relations?: string[], ): Promise { const searchTerm = `%${search}%`; return this.userRepository.count({ where: search ? [ { display_name: ILike(searchTerm), }, { username: ILike(searchTerm), }, { email: ILike(searchTerm), }, ] : undefined, relations, }); } public async getByUsername( username: string, relations?: string[], ): Promise { if (!username) { return null; } return this.userRepository.findOne({ where: { username }, relations }); } public async get( input: string | number, relations?: string[], ): Promise { if (typeof input === 'number') { return this.getById(input, relations); } if (input.includes('@')) { return this.getByEmail(input, relations); } if (input.length === 36 && input.includes('-')) { return this.getByUUID(input, relations); } return this.getByUsername(input, relations); } public async updateUser(user: User): Promise { await this.userRepository.save(user); return user; } public async updateAvatar(user: User, upload: Upload): Promise { if (user.picture) { await this.upload.delete(user.picture); } user.picture = upload; await this.updateUser(user); return user; } public async deleteAvatar(user: User): Promise { if (user.picture) { await this.upload.delete(user.picture); } } public async comparePasswords( hash: string, password: string, ): Promise { return bcrypt.compare(password, hash); } public async hashPassword(password: string): Promise { const salt = await bcrypt.genSalt(10); return bcrypt.hash(password, salt); } public async sendActivationEmail( user: User, redirectTo?: string, ): Promise { const activationToken = await this.userToken.create( user, UserTokenType.ACTIVATION, new Date(Date.now() + 3600 * 1000), ); const params = new URLSearchParams({ token: activationToken.token }); if (redirectTo) { params.append('redirectTo', redirectTo); } try { const content = RegistrationEmail( user.username, `${this.config.get( 'app.base_url', )}/login/activate?${params.toString()}`, ); await this.email.sendEmailTemplate( user.email, 'Activate your account on Icy Network', content, ); } catch (e) { await this.userToken.delete(activationToken); throw e; } } public async sendPasswordEmail(user: User): Promise { const passwordToken = await this.userToken.create( user, UserTokenType.PASSWORD, new Date(Date.now() + 3600 * 1000), ); try { const content = ForgotPasswordEmail( user.username, `${this.config.get('app.base_url')}/login/password?token=${ passwordToken.token }`, ); await this.email.sendEmailTemplate( user.email, 'Reset your password on Icy Network', content, ); } catch (e) { await this.userToken.delete(passwordToken); // silently fail } } public async userPassword(email: string): Promise { const user = await this.getByEmail(email); if (!user || !user.activated) { return; } await this.sendPasswordEmail(user); } public async userRegistration( newUserInfo: { username: string; display_name: string; email: string; password: string; }, redirectTo?: string, ): Promise { if (!!(await this.getByEmail(newUserInfo.email))) { throw new Error('Email is already in use!'); } if (!!(await this.getByUsername(newUserInfo.username))) { throw new Error('Username is already in use!'); } const hashword = await this.hashPassword(newUserInfo.password); const user = new User(); user.email = newUserInfo.email; user.uuid = this.token.createUUID(); user.username = newUserInfo.username; user.display_name = newUserInfo.display_name; user.password = hashword; user.activity_at = new Date(); await this.userRepository.insert(user); try { await this.sendActivationEmail(user, redirectTo); } catch (e) { await this.userRepository.remove(user); throw new Error( 'Failed to send activation email! Please check your email address and try again!', ); } return user; } }