Files
bigcapital/packages/server/src/services/Users/UsersService.ts
2023-04-14 03:41:11 +02:00

316 lines
8.8 KiB
TypeScript

import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { ServiceError } from '@/exceptions';
import {
IEditUserDTO,
ISystemUser,
ITenantUser,
ITenantUserActivatedPayload,
ITenantUserDeletedPayload,
ITenantUserEditedPayload,
ITenantUserInactivatedPayload,
} from '@/interfaces';
import RolesService from '@/services/Roles/RolesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { UserTransformer } from './UserTransformer';
@Service()
export default class UsersService {
@Inject()
private rolesService: RolesService;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private transformer: TransformerInjectable;
/**
* Creates a new user.
* @param {number} tenantId - Tenant id.
* @param {number} userId - User id.
* @param {IUserDTO} editUserDTO - Edit user DTO.
* @return {Promise<ISystemUser>}
*/
public async editUser(
tenantId: number,
userId: number,
editUserDTO: IEditUserDTO,
authorizedUser: ISystemUser
): Promise<any> {
const { User } = this.tenancy.models(tenantId);
const { email } = editUserDTO;
// Retrieve the tenant user or throw not found service error.
const oldTenantUser = await this.getTenantUserOrThrowError(
tenantId,
userId
);
// Validate cannot mutate the authorized user.
this.validateMutateRoleNotAuthorizedUser(
oldTenantUser,
editUserDTO,
authorizedUser
);
// Validate user email should be unique.
await this.validateUserEmailUniquiness(tenantId, email, userId);
// Retrieve the given role or throw not found service error.
const role = await this.rolesService.getRoleOrThrowError(
tenantId,
editUserDTO.roleId
);
// Updates the tenant user.
const tenantUser = await User.query().updateAndFetchById(userId, {
...editUserDTO,
});
// Triggers `onTenantUserEdited` event.
await this.eventPublisher.emitAsync(events.tenantUser.onEdited, {
tenantId,
userId,
editUserDTO,
tenantUser,
oldTenantUser,
} as ITenantUserEditedPayload);
return tenantUser;
}
/**
* Deletes the given user id.
* @param {number} tenantId - Tenant id.
* @param {number} userId - User id.
*/
public async deleteUser(tenantId: number, userId: number): Promise<void> {
const { User } = this.tenancy.models(tenantId);
// Retrieve user details or throw not found service error.
const tenantUser = await this.getTenantUserOrThrowError(tenantId, userId);
// Validate the delete user should not be the last active user.
if (tenantUser.isInviteAccepted) {
await this.validateNotLastUserDelete(tenantId);
}
// Delete user from the storage.
await User.query().findById(userId).delete();
// Triggers `onTenantUserDeleted` event.
await this.eventPublisher.emitAsync(events.tenantUser.onDeleted, {
tenantId,
userId,
tenantUser,
} as ITenantUserDeletedPayload);
}
/**
* Activate the given user id.
* @param {number} tenantId - Tenant id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
public async activateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { User } = this.tenancy.models(tenantId);
// Throw service error if the given user is equals the authorized user.
this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser);
// Retrieve the user or throw not found service error.
const tenantUser = await this.getTenantUserOrThrowError(tenantId, userId);
// Throw serivce error if the user is already activated.
this.throwErrorIfUserActive(tenantUser);
// Marks the tenant user as active.
await User.query().findById(userId).update({ active: true });
// Triggers `onTenantUserActivated` event.
await this.eventPublisher.emitAsync(events.tenantUser.onActivated, {
tenantId,
userId,
authorizedUser,
tenantUser,
} as ITenantUserActivatedPayload);
}
/**
* Inactivate the given user id.
* @param {number} tenantId
* @param {number} userId
* @return {Promise<void>}
*/
public async inactivateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { User } = this.tenancy.models(tenantId);
// Throw service error if the given user is equals the authorized user.
this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser);
// Retrieve the user or throw not found service error.
const tenantUser = await this.getTenantUserOrThrowError(tenantId, userId);
// Throw serivce error if the user is already inactivated.
this.throwErrorIfUserInactive(tenantUser);
// Marks the tenant user as active.
await User.query().findById(userId).update({ active: true });
// Triggers `onTenantUserActivated` event.
await this.eventPublisher.emitAsync(events.tenantUser.onInactivated, {
tenantId,
userId,
authorizedUser,
tenantUser,
} as ITenantUserInactivatedPayload);
}
/**
* Retrieve users list based on the given filter.
* @param {number} tenantId
* @param {object} filter
*/
public async getList(tenantId: number) {
const { User } = this.tenancy.models(tenantId);
const users = await User.query().withGraphFetched('role');
return this.transformer.transform(tenantId, users, new UserTransformer());
}
/**
* Retrieve the given user details.
* @param {number} tenantId - Tenant id.
* @param {number} userId - User id.
*/
public async getUser(tenantId: number, userId: number) {
// Retrieve the system user.
const user = await this.getTenantUserOrThrowError(tenantId, userId);
return user;
}
/**
* Validate user existance throw error in case user was not found.,
* @param {number} tenantId -
* @param {number} userId -
* @returns {ISystemUser}
*/
async getTenantUserOrThrowError(
tenantId: number,
userId: number
): Promise<ITenantUser> {
const { User } = this.tenancy.models(tenantId);
const user = await User.query().findById(userId);
if (!user) {
throw new ServiceError(ERRORS.USER_NOT_FOUND);
}
return user;
}
/**
* Validate the delete user should not be the last user.
* @param {number} tenantId
*/
private async validateNotLastUserDelete(tenantId: number) {
const { User } = this.tenancy.models(tenantId);
const inviteAcceptedUsers = await User.query()
.select(['id'])
.whereNotNull('invite_accepted_at');
if (inviteAcceptedUsers.length === 1) {
throw new ServiceError(ERRORS.CANNOT_DELETE_LAST_USER);
}
}
/**
* Throws service error in case the user was already active.
* @param {ISystemUser} user
* @throws {ServiceError}
*/
private throwErrorIfUserActive(user: ISystemUser) {
if (user.active) {
throw new ServiceError(ERRORS.USER_ALREADY_ACTIVE);
}
}
/**
* Throws service error in case the user was already inactive.
* @param {ISystemUser} user
* @throws {ServiceError}
*/
private throwErrorIfUserInactive(user: ITenantUser) {
if (!user.active) {
throw new ServiceError(ERRORS.USER_ALREADY_INACTIVE);
}
}
/**
* Throw service error in case the given user same the authorized user.
* @param {number} userId
* @param {ISystemUser} authorizedUser
*/
private throwErrorIfUserSameAuthorizedUser(
userId: number,
authorizedUser: ISystemUser
) {
if (userId === authorizedUser.id) {
throw new ServiceError(ERRORS.USER_SAME_THE_AUTHORIZED_USER);
}
}
/**
* Validate the given user email should be unique in the storage.
* @param {string} email
* @param {number} userId
*/
private validateUserEmailUniquiness = async (
tenantId: number,
email: string,
userId: number
) => {
const { User } = this.tenancy.models(tenantId);
const userByEmail = await User.query()
.findOne('email', email)
.whereNot('id', userId);
if (userByEmail) {
throw new ServiceError(ERRORS.EMAIL_ALREADY_EXISTS);
}
};
/**
* Validate the authorized user cannot mutate its role.
* @param {ITenantUser} oldTenantUser
* @param {IEditUserDTO} editUserDTO
* @param {ISystemUser} authorizedUser
*/
validateMutateRoleNotAuthorizedUser(
oldTenantUser: ITenantUser,
editUserDTO: IEditUserDTO,
authorizedUser: ISystemUser
) {
if (
authorizedUser.id === oldTenantUser.systemUserId &&
editUserDTO.roleId !== oldTenantUser.roleId
) {
throw new ServiceError(ERRORS.CANNOT_AUTHORIZED_USER_MUTATE_ROLE);
}
}
}