import { BadRequestException, Injectable, NotFoundException, } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { hashPassword } from "@tria-plc/api-common/utils/argon"; import { EUserStatus } from "@tria-plc/api-common/utils/enums/user.enum"; import { DataSource, In, IsNull, Repository } from "typeorm"; import { Employee, Organization, UserCredential } from "@tria-plc/iamapi-common"; import { Role } from "@tria-plc/iamapi-common/entities/iam/user/role.entity"; import { UserRole } from "@tria-plc/iamapi-common/entities/iam/user/user-role.entity"; import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity"; import { CreateOrganizationUserDto } from "./dto/create-organization-user.dto"; const RESERVED_ROLE_KEYS = new Set([ "super_admin", "organization_admin", "unit_admin", ]); const DEFAULT_USER_PASSWORD = "12345678"; @Injectable() export class BackofficeService { constructor( @InjectRepository(Organization) private readonly organizationRepository: Repository, @InjectRepository(Role) private readonly roleRepository: Repository, @InjectRepository(UserRole) private readonly userRoleRepository: Repository, @InjectRepository(User) private readonly userRepository: Repository, private readonly dataSource: DataSource, ) {} async createOrganizationUser( organizationId: string, dto: CreateOrganizationUserDto, ) { const organizationExists = await this.organizationRepository.exists({ where: { id: organizationId }, }); if (!organizationExists) { throw new NotFoundException("organization_not_found"); } const email = dto.email.trim().toLowerCase(); const username = dto.username.trim().toLowerCase(); const phoneNumber = dto.phoneNumber?.trim() || undefined; const name = { en: dto.name.en.trim(), ...(dto.name.am?.trim() ? { am: dto.name.am.trim() } : {}), }; const existingUsers = await this.userRepository.find({ where: [{ email }, { username }], select: { id: true, email: true, username: true }, }); const emailUser = existingUsers.find((user) => user.email === email); const usernameUser = existingUsers.find((user) => user.username === username); if (emailUser && usernameUser && emailUser.id !== usernameUser.id) { throw new BadRequestException("email_or_username_already_in_use"); } const existingUser = emailUser ?? usernameUser; const hashedPassword = await hashPassword(DEFAULT_USER_PASSWORD); return this.dataSource.transaction(async (manager) => { let user = existingUser; if (!user) { user = await manager.getRepository(User).save( manager.getRepository(User).create({ email, username, phoneNumber, name, isActive: true, hasSetPassword: true, status: EUserStatus.ACCEPTED, }), ); } else { await manager.getRepository(User).update( { id: user.id }, { email, username, phoneNumber, name, isActive: true, hasSetPassword: true, status: EUserStatus.ACCEPTED, }, ); } const activeCredentialExists = await manager.getRepository(UserCredential).exists({ where: { userId: user.id, isActive: true, }, }); if (!activeCredentialExists) { await manager.getRepository(UserCredential).insert({ userId: user.id, password: hashedPassword, isActive: true, }); } let employee = await manager.getRepository(Employee).findOne({ where: { userId: user.id, organizationId, isCurrent: true, }, relations: { user: true, employeePositions: { position: true, }, }, }); if (!employee) { const insertResult = await manager.getRepository(Employee).insert({ userId: user.id, organizationId, isCurrent: true, name, }); employee = await manager.getRepository(Employee).findOne({ where: { id: insertResult.identifiers[0]?.id as string }, relations: { user: true, employeePositions: { position: true, }, }, }); } else { await manager.getRepository(Employee).update( { id: employee.id }, { name }, ); employee = await manager.getRepository(Employee).findOne({ where: { id: employee.id }, relations: { user: true, employeePositions: { position: true, }, }, }); } if (!employee) { throw new NotFoundException("employee_create_failed"); } return employee; }); } async getEmployeeUserRoles(organizationId: string, userId: string) { await this.assertUserBelongsToOrganization(organizationId, userId); const userRoles = await this.userRoleRepository.find({ where: { userId, organizationId, unitId: IsNull(), }, relations: { role: true, }, order: { role: { key: "ASC", }, }, }); return userRoles .map((userRole) => userRole.role) .filter((role): role is Role => Boolean(role)) .map((role) => ({ id: role.id, key: role.key, name: role.name, })); } async replaceEmployeeUserRoles( organizationId: string, userId: string, roleIds: string[], ) { await this.assertUserBelongsToOrganization(organizationId, userId); const uniqueRoleIds = [...new Set(roleIds)]; const roles = uniqueRoleIds.length ? await this.roleRepository.find({ where: { id: In(uniqueRoleIds), }, }) : []; if (roles.length !== uniqueRoleIds.length) { throw new NotFoundException("one_or_more_roles_not_found"); } const reservedRoles = roles.filter((role) => RESERVED_ROLE_KEYS.has(role.key)); if (reservedRoles.length) { throw new BadRequestException("reserved_roles_must_use_admin_actions"); } await this.dataSource.transaction(async (manager) => { await manager.getRepository(UserRole).delete({ userId, organizationId, unitId: IsNull(), }); if (!roles.length) { return; } await manager.getRepository(UserRole).insert( roles.map((role) => ({ userId, roleId: role.id, organizationId, })), ); }); return this.getEmployeeUserRoles(organizationId, userId); } private async assertUserBelongsToOrganization( organizationId: string, userId: string, ) { const exists = await this.userRepository .createQueryBuilder("user") .innerJoin( "user.employee", "employee", "employee.organizationId = :organizationId AND employee.isCurrent = true", { organizationId }, ) .where("user.id = :userId", { userId }) .getExists(); if (!exists) { throw new NotFoundException("user_not_found_in_organization"); } } }