2023-03-30 15:38:55 -04:00
|
|
|
import { UserEntity } from '@app/infra/entities';
|
2022-12-23 21:08:50 +01:00
|
|
|
import {
|
|
|
|
|
BadRequestException,
|
|
|
|
|
ForbiddenException,
|
|
|
|
|
InternalServerErrorException,
|
|
|
|
|
Logger,
|
|
|
|
|
NotFoundException,
|
|
|
|
|
UnauthorizedException,
|
|
|
|
|
} from '@nestjs/common';
|
2022-12-26 23:03:14 -05:00
|
|
|
import { hash } from 'bcrypt';
|
2023-01-11 21:34:36 -05:00
|
|
|
import { constants, createReadStream, ReadStream } from 'fs';
|
2022-12-23 21:08:50 +01:00
|
|
|
import fs from 'fs/promises';
|
2023-01-31 13:11:49 -05:00
|
|
|
import { AuthUserDto } from '../auth';
|
|
|
|
|
import { ICryptoRepository } from '../crypto';
|
2022-12-23 21:08:50 +01:00
|
|
|
import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './dto/create-user.dto';
|
2023-01-11 21:34:36 -05:00
|
|
|
import { IUserRepository, UserListFilter } from './user.repository';
|
2022-12-23 21:08:50 +01:00
|
|
|
|
2022-12-26 23:03:14 -05:00
|
|
|
const SALT_ROUNDS = 10;
|
|
|
|
|
|
2022-12-23 21:08:50 +01:00
|
|
|
export class UserCore {
|
2023-01-27 20:50:07 +00:00
|
|
|
constructor(private userRepository: IUserRepository, private cryptoRepository: ICryptoRepository) {}
|
2022-12-23 21:08:50 +01:00
|
|
|
|
2022-12-26 10:35:52 -05:00
|
|
|
async updateUser(authUser: AuthUserDto, id: string, dto: Partial<UserEntity>): Promise<UserEntity> {
|
2023-04-01 11:43:45 -05:00
|
|
|
if (!authUser.isAdmin && authUser.id !== id) {
|
2022-12-23 21:08:50 +01:00
|
|
|
throw new ForbiddenException('You are not allowed to update this user');
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-01 11:43:45 -05:00
|
|
|
if (!authUser.isAdmin) {
|
|
|
|
|
// Users can never update the isAdmin property.
|
|
|
|
|
delete dto.isAdmin;
|
|
|
|
|
} else if (dto.isAdmin && authUser.id !== id) {
|
|
|
|
|
// Admin cannot create another admin.
|
|
|
|
|
throw new BadRequestException('The server already has an admin');
|
2022-12-23 21:08:50 +01:00
|
|
|
}
|
|
|
|
|
|
2022-12-27 11:36:31 -05:00
|
|
|
if (dto.email) {
|
|
|
|
|
const duplicate = await this.userRepository.getByEmail(dto.email);
|
|
|
|
|
if (duplicate && duplicate.id !== id) {
|
|
|
|
|
throw new BadRequestException('Email already in user by another account');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-23 21:08:50 +01:00
|
|
|
try {
|
2022-12-26 10:35:52 -05:00
|
|
|
if (dto.password) {
|
2023-01-27 20:50:07 +00:00
|
|
|
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
|
2022-12-23 21:08:50 +01:00
|
|
|
}
|
2022-12-28 21:07:04 -06:00
|
|
|
|
2023-01-09 23:08:45 -05:00
|
|
|
return this.userRepository.update(id, dto);
|
2022-12-23 21:08:50 +01:00
|
|
|
} catch (e) {
|
|
|
|
|
Logger.error(e, 'Failed to update user info');
|
|
|
|
|
throw new InternalServerErrorException('Failed to update user info');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createUser(createUserDto: CreateUserDto | CreateAdminDto | CreateUserOAuthDto): Promise<UserEntity> {
|
|
|
|
|
const user = await this.userRepository.getByEmail(createUserDto.email);
|
|
|
|
|
if (user) {
|
|
|
|
|
throw new BadRequestException('User exists');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!(createUserDto as CreateAdminDto).isAdmin) {
|
|
|
|
|
const localAdmin = await this.userRepository.getAdmin();
|
|
|
|
|
if (!localAdmin) {
|
|
|
|
|
throw new BadRequestException('The first registered account must the administrator.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const payload: Partial<UserEntity> = { ...createUserDto };
|
|
|
|
|
if (payload.password) {
|
2022-12-26 23:03:14 -05:00
|
|
|
payload.password = await hash(payload.password, SALT_ROUNDS);
|
2022-12-23 21:08:50 +01:00
|
|
|
}
|
|
|
|
|
return this.userRepository.create(payload);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
Logger.error(e, 'Create new user');
|
|
|
|
|
throw new InternalServerErrorException('Failed to register new user');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async get(userId: string, withDeleted?: boolean): Promise<UserEntity | null> {
|
|
|
|
|
return this.userRepository.get(userId, withDeleted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getAdmin(): Promise<UserEntity | null> {
|
|
|
|
|
return this.userRepository.getAdmin();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | null> {
|
|
|
|
|
return this.userRepository.getByEmail(email, withPassword);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getByOAuthId(oauthId: string): Promise<UserEntity | null> {
|
|
|
|
|
return this.userRepository.getByOAuthId(oauthId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getUserProfileImage(user: UserEntity): Promise<ReadStream> {
|
|
|
|
|
if (!user.profileImagePath) {
|
|
|
|
|
throw new NotFoundException('User does not have a profile image');
|
|
|
|
|
}
|
|
|
|
|
await fs.access(user.profileImagePath, constants.R_OK | constants.W_OK);
|
|
|
|
|
return createReadStream(user.profileImagePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getList(filter?: UserListFilter): Promise<UserEntity[]> {
|
|
|
|
|
return this.userRepository.getList(filter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createProfileImage(authUser: AuthUserDto, filePath: string): Promise<UserEntity> {
|
|
|
|
|
// TODO: do we need to do this? Maybe we can trust the authUser
|
|
|
|
|
const user = await this.userRepository.get(authUser.id);
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return this.userRepository.update(user.id, { profileImagePath: filePath });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
Logger.error(e, 'Create User Profile Image');
|
|
|
|
|
throw new InternalServerErrorException('Failed to create new user profile image');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async restoreUser(authUser: AuthUserDto, userToRestore: UserEntity): Promise<UserEntity> {
|
|
|
|
|
// TODO: do we need to do this? Maybe we can trust the authUser
|
|
|
|
|
const requestor = await this.userRepository.get(authUser.id);
|
|
|
|
|
if (!requestor) {
|
|
|
|
|
throw new UnauthorizedException('Requestor not found');
|
|
|
|
|
}
|
|
|
|
|
if (!requestor.isAdmin) {
|
|
|
|
|
throw new ForbiddenException('Unauthorized');
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return this.userRepository.restore(userToRestore);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
Logger.error(e, 'Failed to restore deleted user');
|
|
|
|
|
throw new InternalServerErrorException('Failed to restore deleted user');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteUser(authUser: AuthUserDto, userToDelete: UserEntity): Promise<UserEntity> {
|
|
|
|
|
// TODO: do we need to do this? Maybe we can trust the authUser
|
|
|
|
|
const requestor = await this.userRepository.get(authUser.id);
|
|
|
|
|
if (!requestor) {
|
|
|
|
|
throw new UnauthorizedException('Requestor not found');
|
|
|
|
|
}
|
|
|
|
|
if (!requestor.isAdmin) {
|
|
|
|
|
throw new ForbiddenException('Unauthorized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (userToDelete.isAdmin) {
|
|
|
|
|
throw new ForbiddenException('Cannot delete admin user');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return this.userRepository.delete(userToDelete);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
Logger.error(e, 'Failed to delete user');
|
|
|
|
|
throw new InternalServerErrorException('Failed to delete user');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|