Auth completo (Argon2id + JWT/refresh rotativo), permissões, Companies e OpenAPI
- 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>
This commit is contained in:
102
apps/api/src/companies/companies.service.ts
Normal file
102
apps/api/src/companies/companies.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { companiesRepo, auditRepo, type Company } from "@eden/database";
|
||||
import { onlyDigits, isValidCNPJ } from "@eden/domain-shared";
|
||||
import type { CreateCompanyDto } from "./dto/create-company.dto.js";
|
||||
import type { UpdateCompanyDto } from "./dto/update-company.dto.js";
|
||||
|
||||
@Injectable()
|
||||
export class CompaniesService {
|
||||
list(includeInactive: boolean): Promise<Company[]> {
|
||||
return companiesRepo.listCompanies(includeInactive);
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Company> {
|
||||
const company = await companiesRepo.getCompanyById(id);
|
||||
if (!company) throw new NotFoundException("Empresa não encontrada");
|
||||
return company;
|
||||
}
|
||||
|
||||
async create(dto: CreateCompanyDto, actorId: string): Promise<Company> {
|
||||
this.assertValidCnpjIfPresent(dto.cnpj);
|
||||
const company = await companiesRepo.createCompany({
|
||||
companyName: dto.companyName,
|
||||
tradeName: dto.tradeName,
|
||||
cnpj: dto.cnpj ? onlyDigits(dto.cnpj) : undefined,
|
||||
stateRegistration: dto.stateRegistration,
|
||||
municipalRegistration: dto.municipalRegistration,
|
||||
addressZip: dto.addressZip,
|
||||
addressStreet: dto.addressStreet,
|
||||
addressNumber: dto.addressNumber,
|
||||
addressComplement: dto.addressComplement,
|
||||
addressNeighborhood: dto.addressNeighborhood,
|
||||
addressCity: dto.addressCity,
|
||||
addressState: dto.addressState,
|
||||
addressCountry: dto.addressCountry,
|
||||
phone: dto.phone,
|
||||
email: dto.email,
|
||||
website: dto.website,
|
||||
legalRepName: dto.legalRepName,
|
||||
legalRepCpf: dto.legalRepCpf ? onlyDigits(dto.legalRepCpf) : undefined,
|
||||
legalRepRole: dto.legalRepRole,
|
||||
legalRepEmail: dto.legalRepEmail,
|
||||
createdBy: actorId,
|
||||
});
|
||||
|
||||
await auditRepo.appendAuditEvent({
|
||||
actorType: "user",
|
||||
actorId,
|
||||
action: "company.created",
|
||||
resourceType: "company",
|
||||
resourceId: company.id,
|
||||
});
|
||||
|
||||
return company;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCompanyDto, actorId: string): Promise<Company> {
|
||||
await this.get(id); // 404 if missing
|
||||
this.assertValidCnpjIfPresent(dto.cnpj);
|
||||
|
||||
const company = await companiesRepo.updateCompany(id, {
|
||||
companyName: dto.companyName,
|
||||
tradeName: dto.tradeName,
|
||||
cnpj: dto.cnpj ? onlyDigits(dto.cnpj) : undefined,
|
||||
stateRegistration: dto.stateRegistration,
|
||||
municipalRegistration: dto.municipalRegistration,
|
||||
addressZip: dto.addressZip,
|
||||
addressStreet: dto.addressStreet,
|
||||
addressNumber: dto.addressNumber,
|
||||
addressComplement: dto.addressComplement,
|
||||
addressNeighborhood: dto.addressNeighborhood,
|
||||
addressCity: dto.addressCity,
|
||||
addressState: dto.addressState,
|
||||
addressCountry: dto.addressCountry,
|
||||
phone: dto.phone,
|
||||
email: dto.email,
|
||||
website: dto.website,
|
||||
legalRepName: dto.legalRepName,
|
||||
legalRepCpf: dto.legalRepCpf ? onlyDigits(dto.legalRepCpf) : undefined,
|
||||
legalRepRole: dto.legalRepRole,
|
||||
legalRepEmail: dto.legalRepEmail,
|
||||
isActive: dto.isActive,
|
||||
});
|
||||
if (!company) throw new NotFoundException("Empresa não encontrada");
|
||||
|
||||
await auditRepo.appendAuditEvent({
|
||||
actorType: "user",
|
||||
actorId,
|
||||
action: "company.updated",
|
||||
resourceType: "company",
|
||||
resourceId: id,
|
||||
metadata: { fields: Object.keys(dto) },
|
||||
});
|
||||
|
||||
return company;
|
||||
}
|
||||
|
||||
private assertValidCnpjIfPresent(cnpj: string | undefined): void {
|
||||
if (cnpj && !isValidCNPJ(cnpj)) {
|
||||
throw new BadRequestException("CNPJ inválido");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user