Files
dh-inspeccion-v2/api-v3/src/administration/roles/roles.service.ts
T

310 lines
8.7 KiB
TypeScript

import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { DataSource, EntityManager, In } from 'typeorm';
import { AuditService } from '../../audit/audit.service';
import type {
AuthPrincipal,
RequestWithContext,
} from '../../common/http/request-context';
import {
AuditAction,
Permission,
Role,
RolePermission,
} from '../../database/entities';
import {
administrationAuditContext,
assertAdministrativeRecoveryRemains,
isUniqueViolation,
} from '../common/administration-audit';
import type { CreateRoleDto } from './dto/create-role.dto';
import type { ReplaceRolePermissionsDto } from './dto/replace-role-permissions.dto';
import type { UpdateRoleDto } from './dto/update-role.dto';
export interface PermissionView {
id: string;
code: string;
description: string;
}
export interface AdministrativeRoleView {
id: string;
code: string;
name: string;
description: string;
isSystem: boolean;
createdAt: Date;
updatedAt: Date;
userCount: number;
permissions: PermissionView[];
}
function roleNotFound(): NotFoundException {
return new NotFoundException({
code: 'ROLE_NOT_FOUND',
message: 'Rol no encontrado',
});
}
function permissionSelectionInvalid(): BadRequestException {
return new BadRequestException({
code: 'PERMISSION_NOT_FOUND',
message: 'Uno o más permisos no existen',
});
}
@Injectable()
export class RolesService {
constructor(
private readonly dataSource: DataSource,
private readonly audit: AuditService,
) {}
async list(): Promise<{ data: AdministrativeRoleView[] }> {
const rows = (await this.dataSource.query(this.roleViewQuery(''), [])) as
AdministrativeRoleView[];
return { data: rows };
}
async listPermissions(): Promise<{ data: PermissionView[] }> {
const rows = (await this.dataSource.query(
`
SELECT id, code, description
FROM permissions
ORDER BY code ASC
`,
)) as PermissionView[];
return { data: rows };
}
async getById(id: string): Promise<AdministrativeRoleView> {
return this.dataSource.transaction((manager) =>
this.loadRoleView(manager, id),
);
}
async create(
dto: CreateRoleDto,
principal: AuthPrincipal,
request: RequestWithContext,
): Promise<AdministrativeRoleView> {
try {
return await this.dataSource.transaction(async (manager) => {
const permissions = await this.resolvePermissions(
manager,
dto.permissionIds,
);
const role = manager.getRepository(Role).create({
code: dto.code.trim().toLowerCase(),
name: dto.name.trim(),
description: dto.description.trim(),
isSystem: false,
});
await manager.getRepository(Role).save(role);
await this.insertPermissions(manager, role.id, permissions);
const created = await this.loadRoleView(manager, role.id);
await this.audit.record(
{
...administrationAuditContext(principal, request),
action: AuditAction.ROLE_CREATED,
entityType: 'role',
entityId: role.id,
afterData: { ...created },
},
manager,
);
return created;
});
} catch (error) {
if (isUniqueViolation(error)) throw this.roleConflict();
throw error;
}
}
async update(
id: string,
dto: UpdateRoleDto,
principal: AuthPrincipal,
request: RequestWithContext,
): Promise<AdministrativeRoleView> {
if (dto.name === undefined && dto.description === undefined) {
throw new BadRequestException({
code: 'NO_CHANGES',
message: 'No se recibieron cambios',
});
}
return this.dataSource.transaction(async (manager) => {
const role = await this.lockRole(manager, id);
const before = await this.loadRoleView(manager, id);
if (dto.name !== undefined) role.name = dto.name.trim();
if (dto.description !== undefined) {
role.description = dto.description.trim();
}
await manager.getRepository(Role).save(role);
const updated = await this.loadRoleView(manager, id);
await this.audit.record(
{
...administrationAuditContext(principal, request),
action: AuditAction.ROLE_UPDATED,
entityType: 'role',
entityId: id,
beforeData: {
name: before.name,
description: before.description,
},
afterData: {
name: updated.name,
description: updated.description,
},
},
manager,
);
return updated;
});
}
async replacePermissions(
id: string,
dto: ReplaceRolePermissionsDto,
principal: AuthPrincipal,
request: RequestWithContext,
): Promise<AdministrativeRoleView> {
return this.dataSource.transaction(async (manager) => {
await this.lockRole(manager, id);
const permissions = await this.resolvePermissions(
manager,
dto.permissionIds,
);
const before = await this.loadRoleView(manager, id);
const beforeIds = before.permissions
.map((permission) => permission.id)
.sort();
const afterIds = permissions.map((permission) => permission.id).sort();
if (beforeIds.join(',') === afterIds.join(',')) return before;
await manager.getRepository(RolePermission).delete({ roleId: id });
await this.insertPermissions(manager, id, permissions);
await assertAdministrativeRecoveryRemains(manager);
const updated = await this.loadRoleView(manager, id);
await this.audit.record(
{
...administrationAuditContext(principal, request),
action: AuditAction.ROLE_PERMISSIONS_CHANGED,
entityType: 'role',
entityId: id,
beforeData: { permissions: before.permissions },
afterData: { permissions: updated.permissions },
},
manager,
);
return updated;
});
}
private async lockRole(manager: EntityManager, id: string): Promise<Role> {
const role = await manager
.getRepository(Role)
.createQueryBuilder('role')
.where('role.id = :id', { id })
.setLock('pessimistic_write')
.getOne();
if (!role) throw roleNotFound();
return role;
}
private async resolvePermissions(
manager: EntityManager,
permissionIds: string[],
): Promise<Permission[]> {
const uniqueIds = [...new Set(permissionIds)];
if (uniqueIds.length === 0) return [];
const permissions = await manager.getRepository(Permission).find({
where: { id: In(uniqueIds) },
order: { code: 'ASC' },
});
if (permissions.length !== uniqueIds.length) {
throw permissionSelectionInvalid();
}
return permissions;
}
private async insertPermissions(
manager: EntityManager,
roleId: string,
permissions: Permission[],
): Promise<void> {
if (permissions.length === 0) return;
const assignments = permissions.map((permission) =>
manager.getRepository(RolePermission).create({
roleId,
permissionId: permission.id,
}),
);
await manager.getRepository(RolePermission).save(assignments);
}
private async loadRoleView(
manager: EntityManager,
id: string,
): Promise<AdministrativeRoleView> {
const rows = (await manager.query(
this.roleViewQuery('WHERE role.id = $1'),
[id],
)) as AdministrativeRoleView[];
if (!rows[0]) throw roleNotFound();
return rows[0];
}
private roleViewQuery(where: string): string {
return `
SELECT
role.id,
role.code,
role.name,
role.description,
role.is_system AS "isSystem",
role.created_at AS "createdAt",
role.updated_at AS "updatedAt",
(
SELECT COUNT(*)::integer
FROM user_roles user_role
WHERE user_role.role_id = role.id
) AS "userCount",
COALESCE(
(
SELECT JSONB_AGG(
JSONB_BUILD_OBJECT(
'id', permission.id,
'code', permission.code,
'description', permission.description
) ORDER BY permission.code
)
FROM role_permissions role_permission
INNER JOIN permissions permission
ON permission.id = role_permission.permission_id
WHERE role_permission.role_id = role.id
),
'[]'::jsonb
) AS permissions
FROM roles role
${where}
ORDER BY role.is_system DESC, role.code ASC
`;
}
private roleConflict(): ConflictException {
return new ConflictException({
code: 'ROLE_ALREADY_EXISTS',
message: 'Ya existe un rol con ese código',
});
}
}