- apps/api/src/users/: invite (senha temporária, mesma entropia do legado), PATCH (role/isActive, com checagem de peso), reset de senha administrativo, troca de senha self-service, grant/revoke de acesso a aplicação — cada ação sensível auditada. - apps/api/src/roles/: CRUD de papéis e gestão de role_permissions, exclusivo super_admin (SuperAdminGuard), mirando o requireSuperAdmin do legado — deliberadamente não generalizado para o modelo de permissão resource+action+scope, por ser a raiz do próprio sistema de autorização (documentado em roles.service.ts). - apps/api/src/auth/role-weight.ts: assertCanAssignRole/assertCanActOnUser (eden.md §1.6) — um ator nunca atribui papel nem mexe em usuário com peso maior que o seu; super_admin sempre passa. - .gitignore: .claude/worktrees/ (worktrees temporários de subagentes paralelos nunca devem virar submódulo do repo principal). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
|
|
import { rolesRepo, auditRepo } from "@eden/database";
|
|
import { SUPER_ADMIN_ROLE } from "../auth/permissions.js";
|
|
import type { CreateRoleDto } from "./dto/create-role.dto.js";
|
|
import type { UpdateRoleDto } from "./dto/update-role.dto.js";
|
|
import type { SetRolePermissionsDto } from "./dto/set-role-permissions.dto.js";
|
|
|
|
@Injectable()
|
|
export class RolesService {
|
|
list() {
|
|
return rolesRepo.listRoles();
|
|
}
|
|
|
|
async getPermissions(roleKey: string) {
|
|
if (roleKey === SUPER_ADMIN_ROLE) {
|
|
// super_admin never has rows here — bypass is hardcoded, not data-driven (eden.md §2.1 item 4).
|
|
throw new BadRequestException("super_admin não usa role_permissions — acesso é total por definição.");
|
|
}
|
|
const role = await rolesRepo.getRoleByKey(roleKey);
|
|
if (!role) throw new NotFoundException("Papel não encontrado");
|
|
return rolesRepo.getRolePermissions(roleKey);
|
|
}
|
|
|
|
async create(dto: CreateRoleDto, actorId: string) {
|
|
const existing = await rolesRepo.getRoleByKey(dto.key);
|
|
if (existing) throw new ConflictException("Já existe um papel com essa chave");
|
|
|
|
const role = await rolesRepo.createRole({
|
|
key: dto.key,
|
|
label: dto.label,
|
|
weight: dto.weight,
|
|
createdBy: actorId,
|
|
});
|
|
|
|
await auditRepo.appendAuditEvent({
|
|
actorType: "user",
|
|
actorId,
|
|
action: "role.created",
|
|
resourceType: "role",
|
|
resourceId: role.key,
|
|
metadata: { weight: role.weight },
|
|
});
|
|
|
|
return role;
|
|
}
|
|
|
|
async update(key: string, dto: UpdateRoleDto, actorId: string) {
|
|
const role = await this.assertEditableRole(key);
|
|
const updated = await rolesRepo.updateRole(key, dto);
|
|
if (!updated) throw new NotFoundException("Papel não encontrado");
|
|
|
|
await auditRepo.appendAuditEvent({
|
|
actorType: "user",
|
|
actorId,
|
|
action: "role.updated",
|
|
resourceType: "role",
|
|
resourceId: key,
|
|
metadata: { fields: Object.keys(dto), previousWeight: role.weight },
|
|
});
|
|
|
|
return updated;
|
|
}
|
|
|
|
async delete(key: string, actorId: string) {
|
|
const role = await this.assertEditableRole(key);
|
|
if (role.is_system) {
|
|
throw new BadRequestException("Papéis do sistema não podem ser excluídos");
|
|
}
|
|
const usersWithRole = await rolesRepo.countUsersWithRole(key);
|
|
if (usersWithRole > 0) {
|
|
throw new ConflictException(
|
|
`Não é possível excluir: ${usersWithRole} usuário(s) ainda têm este papel`,
|
|
);
|
|
}
|
|
await rolesRepo.deleteRole(key);
|
|
await auditRepo.appendAuditEvent({
|
|
actorType: "user",
|
|
actorId,
|
|
action: "role.deleted",
|
|
resourceType: "role",
|
|
resourceId: key,
|
|
});
|
|
}
|
|
|
|
async setPermissions(key: string, dto: SetRolePermissionsDto, actorId: string) {
|
|
await this.assertEditableRole(key);
|
|
await rolesRepo.setRolePermissions(key, dto.permissions, actorId);
|
|
await auditRepo.appendAuditEvent({
|
|
actorType: "user",
|
|
actorId,
|
|
action: "role.permissions_set",
|
|
resourceType: "role",
|
|
resourceId: key,
|
|
metadata: { count: dto.permissions.length },
|
|
});
|
|
return rolesRepo.getRolePermissions(key);
|
|
}
|
|
|
|
/** super_admin is never editable/deletable via API — always promoted/managed via migration (eden.md §2.1 item 7). */
|
|
private async assertEditableRole(key: string) {
|
|
if (key === SUPER_ADMIN_ROLE) {
|
|
throw new BadRequestException("O papel super_admin não pode ser alterado via API");
|
|
}
|
|
const role = await rolesRepo.getRoleByKey(key);
|
|
if (!role) throw new NotFoundException("Papel não encontrado");
|
|
return role;
|
|
}
|
|
}
|