- Auth (apps/api/src/auth/): Argon2id com parâmetros versionados
(ADR-0005), access token JWT de 15min, refresh token opaco de 256 bits
com rotação e detecção de replay (reuso de token já rotacionado revoga
toda a cadeia de sessões do usuário). Endpoints /api/v1/auth/
{login,refresh,logout,me}.
- Permissões resource+action+scope (ADR-0004) via PermissionGuard,
resolvidas do banco a cada request (nunca cacheadas no JWT) — mudança
de papel tem efeito imediato. super_admin com bypass hardcoded,
espelhando a trava já existente no banco.
- Companies (migration + CRUD): leitura aberta a qualquer autenticado,
escrita restrita a companies:manage; validação de CNPJ via
packages/domain-shared (dígito verificador).
- SeedService: cria o primeiro super_admin a partir de
EDEN_SUPERADMIN_EMAIL/PASSWORD só quando `users` está vazia.
- OpenAPI/Swagger em /api/docs; ValidationPipe global com
whitelist+forbidNonWhitelisted (proteção contra mass-assignment).
- packages/database: repositórios (roles/users/sessions/audit/companies)
reutilizáveis por api e worker.
Validado manualmente contra a stack real (não só typecheck): login com
senha errada → 401 genérico; refresh rotaciona e invalida o token
anterior; reuso do token antigo → replay detectado, cadeia revogada;
logout idempotente; audit log com a sequência completa de eventos;
CNPJ inválido rejeitado; mass-assignment rejeitado.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import { rolesRepo, type RolePermission } from "@eden/database";
|
|
|
|
export const SUPER_ADMIN_ROLE = "super_admin";
|
|
|
|
export type ResolvedPermissions =
|
|
| { superAdmin: true }
|
|
| { superAdmin: false; grants: RolePermission[] };
|
|
|
|
/**
|
|
* super_admin is never looked up in role_permissions — hardcoded bypass,
|
|
* mirrored from the legacy system and enforced again at the DB level
|
|
* (docs/adr/0004-permission-model.md, chk_role_permissions_no_super_admin).
|
|
*/
|
|
export async function resolvePermissions(roleKey: string): Promise<ResolvedPermissions> {
|
|
if (roleKey === SUPER_ADMIN_ROLE) return { superAdmin: true };
|
|
const grants = await rolesRepo.getRolePermissions(roleKey);
|
|
return { superAdmin: false, grants };
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum scope the actor holds for (resource, action), or
|
|
* null if they hold none. Scope precedence — broader wins when a role
|
|
* somehow has more than one row for the same resource+action (shouldn't
|
|
* happen given the UNIQUE constraint per scope, but a role could have
|
|
* both e.g. 'own' and 'all' rows intentionally):
|
|
*/
|
|
const SCOPE_RANK: Record<RolePermission["scope"], number> = {
|
|
own: 1,
|
|
team: 2,
|
|
reseller: 3,
|
|
legal_entity: 4,
|
|
all: 5,
|
|
};
|
|
|
|
export function maxGrantedScope(
|
|
permissions: ResolvedPermissions,
|
|
resource: string,
|
|
action: RolePermission["action"],
|
|
): RolePermission["scope"] | "all" | null {
|
|
if (permissions.superAdmin) return "all";
|
|
const matches = permissions.grants.filter((g) => g.resource === resource && g.action === action);
|
|
if (matches.length === 0) return null;
|
|
return matches.reduce<RolePermission["scope"]>(
|
|
(best, g) => (SCOPE_RANK[g.scope] > SCOPE_RANK[best] ? g.scope : best),
|
|
matches[0]!.scope,
|
|
);
|
|
}
|
|
|
|
export function hasPermission(
|
|
permissions: ResolvedPermissions,
|
|
resource: string,
|
|
action: RolePermission["action"],
|
|
): boolean {
|
|
return maxGrantedScope(permissions, resource, action) !== null;
|
|
}
|