Files
eden/apps/api/src/roles/roles.service.ts
Matheus (Handix) 8f638f2743 CRUD de Usuários e Papéis (role weight, permissões, gitignore de worktrees)
- 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>
2026-09-03 09:40:28 -03:00

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;
}
}