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} */ public async editUser( tenantId: number, userId: number, editUserDTO: IEditUserDTO, authorizedUser: ISystemUser ): Promise { 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 { 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} */ public async activateUser( tenantId: number, userId: number, authorizedUser: ISystemUser ): Promise { 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} */ public async inactivateUser( tenantId: number, userId: number, authorizedUser: ISystemUser ): Promise { 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 { 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); } } }