feat: add authentication and RBAC
- packages/database: schema Prisma (users/sessions/roles/permissions/
user_roles/role_permissions/audit_logs/password_reset_tokens), migration
inicial e seed (permissoes+perfis+bootstrap super_admin com senha
aleatoria em FIRST_LOGIN.txt). Decisao de ORM (Prisma) documentada em
docs/ARCHITECTURE.md
- packages/shared: catalogo de permissoes (fonte unica usada por seed e API)
- apps/api: NestJS 11 + Fastify
- autenticacao: Argon2id, access JWT + refresh token opaco com rotacao,
cookies HttpOnly/SameSite=Lax, change/forgot/reset password
- rate limiting progressivo de login via Redis (bloqueio crescente por IP)
- RBAC reforcado no backend (PermissionsGuard), protecao contra
auto-elevacao de privilegio
- auditoria (audit_logs) nas acoes sensiveis, com redacao de segredos
- health checks reais (postgres+redis), swagger desabilitavel, logs
estruturados JSON com request_id de correlacao, filtro global de
excecoes sem vazar erro cru
- infrastructure/docker/api.Dockerfile: build multi-stage do monorepo pnpm
- docker-compose.yml: servico api na rede interna, sem porta publicada
Testado via containers reais: login, /me, refresh, change-password,
rate limit (7 tentativas -> 429), RBAC (nega/permite), bloqueio de
auto-elevacao (403), audit log populado, health checks, lint e testes
unitarios passando.
This commit is contained in:
97
apps/api/src/audit/audit.service.ts
Normal file
97
apps/api/src/audit/audit.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Prisma } from '@b2bcall/database';
|
||||
import type { QueryAuditDto } from './dto/query-audit.dto';
|
||||
|
||||
export interface AuditEntry {
|
||||
userId?: string | null;
|
||||
action: string;
|
||||
entityType?: string;
|
||||
entityId?: string;
|
||||
before?: Prisma.InputJsonValue | null;
|
||||
after?: Prisma.InputJsonValue | null;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
// Campos que nunca devem ser persistidos em texto puro no before/after do
|
||||
// audit log (agente.md seção 12: "nunca salvar segredos abertos").
|
||||
const SENSITIVE_KEYS = new Set([
|
||||
'password',
|
||||
'passwordHash',
|
||||
'secret',
|
||||
'token',
|
||||
'refreshToken',
|
||||
'accessToken',
|
||||
'amiSecret',
|
||||
'ariSecret',
|
||||
]);
|
||||
|
||||
function redact(value: unknown): unknown {
|
||||
if (value === null || value === undefined) return value;
|
||||
if (Array.isArray(value)) return value.map(redact);
|
||||
if (typeof value === 'object') {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
||||
out[key] = SENSITIVE_KEYS.has(key) ? '[REDACTED]' : redact(val);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async log(entry: AuditEntry): Promise<void> {
|
||||
await this.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: entry.userId ?? null,
|
||||
action: entry.action,
|
||||
entityType: entry.entityType,
|
||||
entityId: entry.entityId,
|
||||
before:
|
||||
(redact(entry.before ?? null) as Prisma.InputJsonValue) ??
|
||||
Prisma.JsonNull,
|
||||
after:
|
||||
(redact(entry.after ?? null) as Prisma.InputJsonValue) ??
|
||||
Prisma.JsonNull,
|
||||
ipAddress: entry.ipAddress,
|
||||
userAgent: entry.userAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Paginação sempre server-side (agente.md seção 54) — audit_logs cresce
|
||||
// sem limite, nunca um SELECT * sem filtro/paginação.
|
||||
async query(query: QueryAuditDto) {
|
||||
const where: Prisma.AuditLogWhereInput = {
|
||||
userId: query.userId,
|
||||
action: query.action,
|
||||
entityType: query.entityType,
|
||||
createdAt: {
|
||||
gte: query.from ? new Date(query.from) : undefined,
|
||||
lte: query.to ? new Date(query.to) : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const [total, items] = await this.prisma.$transaction([
|
||||
this.prisma.auditLog.count({ where }),
|
||||
this.prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (query.page - 1) * query.pageSize,
|
||||
take: query.pageSize,
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
items: items.map((item) => ({ ...item, id: item.id.toString() })),
|
||||
total,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user