icynet-auth-server/src/modules/objects/user/user.service.ts

360 lines
9.5 KiB
TypeScript

import {
BadRequestException,
Inject,
Injectable,
Logger,
} 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';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InvitationEmail } from './email/invitation.email';
@Injectable()
export class UserService {
private readonly logger = new Logger(UserService.name);
constructor(
@Inject('USER_REPOSITORY')
private userRepository: Repository<User>,
private userToken: UserTokenService,
private token: TokenService,
private email: EmailService,
private config: ConfigurationService,
private upload: UploadService,
@Inject(CACHE_MANAGER) private readonly cache: Cache,
) {}
public async getById(id: number, relations?: string[]): Promise<User> {
if (!id) {
return null;
}
return this.userRepository.findOne({ where: { id }, relations });
}
public async getByUUID(uuid: string, relations?: string[]): Promise<User> {
if (!uuid) {
return null;
}
return this.userRepository.findOne({ where: { uuid }, relations });
}
public async getByEmail(email: string, relations?: string[]): Promise<User> {
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<number> {
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<User> {
if (!username) {
return null;
}
return this.userRepository.findOne({ where: { username }, relations });
}
public async get(
input: string | number,
relations?: string[],
): Promise<User> {
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<User> {
await this.userRepository.save(user);
return user;
}
public async updateAvatar(user: User, upload: Upload): Promise<User> {
if (user.picture) {
await this.upload.delete(user.picture);
}
user.picture = upload;
await this.updateUser(user);
this.logger.log(`User ID: ${user.id} avatar has been updated`);
return user;
}
public async deleteAvatar(user: User): Promise<void> {
if (user.picture) {
await this.upload.delete(user.picture);
this.logger.log(`User ID: ${user.id} avatar has been deleted`);
}
}
public async comparePasswords(
hash: string,
password: string,
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
public async hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(10);
return bcrypt.hash(password, salt);
}
public async sendActivationEmail(
user: User,
redirectTo?: string,
): Promise<void> {
this.logger.log(
`Sending an activation email to User ID: ${user.id} (${user.email})`,
);
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<string>(
'app.base_url',
)}/login/activate?${params.toString()}`,
);
await this.email.sendEmailTemplate(
user.email,
'Activate your account on Icy Network',
content,
);
this.logger.log(
`Sending an activation email to User ID: ${user.id} (${user.email}) was successful.`,
);
} catch (e) {
this.logger.error(
`Sending an activation email to User ID: ${user.id} (${user.email}) failed`,
);
await this.userToken.delete(activationToken);
throw e;
}
}
public async sendPasswordEmail(user: User): Promise<void> {
this.logger.log(
`Sending a password reset email to User ID: ${user.id} (${user.email})`,
);
const passwordToken = await this.userToken.create(
user,
UserTokenType.PASSWORD,
new Date(Date.now() + 3600 * 1000),
);
try {
const content = ForgotPasswordEmail(
user.username,
`${this.config.get<string>('app.base_url')}/login/password?token=${
passwordToken.token
}`,
);
await this.email.sendEmailTemplate(
user.email,
'Reset your password on Icy Network',
content,
);
this.logger.log(
`Sent a password reset email to User ID: ${user.id} (${user.email}) successfully`,
);
} catch (e) {
this.logger.error(
`Sending a password reset email to User ID: ${user.id} (${user.email}) failed: ${e}`,
);
await this.userToken.delete(passwordToken);
// silently fail
}
}
public async userPassword(email: string): Promise<void> {
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,
activate = false,
): Promise<User> {
this.logger.log(
`Starting registration of new user ${newUserInfo.username} (${newUserInfo.email})`,
);
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();
user.activated = activate;
await this.userRepository.save(user);
if (!user.activated) {
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!',
);
}
}
this.logger.log(
`Registered a new user ${newUserInfo.username} (${newUserInfo.email}) ID: ${user.id}`,
);
return user;
}
public async issueRegistrationToken(email: string) {
this.logger.log(`Issuing a new registration token for ${email}`);
const existingUser = await this.getByEmail(email);
if (existingUser) {
throw new BadRequestException('User by email already exists');
}
const newToken = this.token.generateString(64);
await this.cache.set(
`register-${newToken}`,
email,
7 * 24 * 60 * 60 * 1000, // 7 days
);
try {
const content = InvitationEmail(
`${this.config.get<string>('app.base_url')}/register?token=${newToken}`,
);
await this.email.sendEmailTemplate(
email,
'You have been invited to create an account on Icy Network',
content,
);
this.logger.log(
`Issuing a new registration token for ${email} was successful`,
);
} catch (error) {
this.logger.error(
`Issuing a new registration token for ${email} failed: ${error}`,
);
await this.cache.del(`register-${newToken}`);
throw error;
}
}
public async checkRegistrationToken(token: string) {
return await this.cache.get<string>(`register-${token}`);
}
public async invalidateRegistrationToken(token: string) {
return await this.cache.del(`register-${token}`);
}
}