feat/CRUD de leitura para Logs de Auditoria#406
Conversation
WalkthroughAdds a new AuditLog feature: registers AuditLogModule in AppModule; introduces AuditLogController with list and summary endpoints; implements AuditLogService with Prisma-backed filtering, JWT-based pagination and grouping; adds filter DTOs and response entities for list and summary. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Controller as AuditLogController
participant Service as AuditLogService
participant Prisma as Prisma.logGenerico
participant JWT as JwtService
rect rgb(245,248,255)
Note over Client,Controller: List logs (GET /audit-log)
Client->>Controller: GET /audit-log?filters&token_proxima_pagina
Controller->>Service: findAll(filters)
alt token provided
Service->>JWT: verify next_page_token
JWT-->>Service: {offset, ipp}
else no token
Service-->>Service: use defaults (offset=0, ipp)
end
Service->>Prisma: findMany(where, skip=offset, take=ipp+1, include pessoa)
Prisma-->>Service: rows
Service->>JWT: sign next_page_token (offset+ipp)
JWT-->>Service: token
Service-->>Controller: {linhas, tem_mais, token_ttl, token_proxima_pagina}
Controller-->>Client: 200 JSON
end
rect rgb(245,255,245)
Note over Client,Controller: Summary (GET /audit-log/summary)
Client->>Controller: GET /audit-log/summary?filters&group_by_*
Controller->>Service: getSummary(filters, groupBy)
Service->>Service: validate groupBy fields
Service->>Prisma: groupBy(selected fields, where, _count)
Prisma-->>Service: grouped results
Service-->>Controller: {linhas: [AuditLogSummaryRow...]}
Controller-->>Client: 200 JSON
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (13)
backend/src/audit-log/audit-log.module.ts (1)
15-16: Consider exporting the service only if reused by other modules.If other modules will consume AuditLogService (e.g., reporting), export it now to avoid later churn. Otherwise, current scoping is fine.
backend/src/audit-log/audit-log.controller.ts (3)
4-4: Prefer project’s path aliases or relative imports consistently.
import { Roles } from "src/auth/...";may diverge from the project’s prevalent relative style. Align to repo convention to avoid path resolution surprises.-import { Roles } from "src/auth/decorators/roles.decorator"; +import { Roles } from "../auth/decorators/roles.decorator";
13-18: Annotate response type and OpenAPI metadata for list endpoint.Helps consumers and avoids “any” at call sites. If you have a PaginatedDto, expose it here and in Swagger.
- async findAll(@Query() filters: FilterAuditLogDto) { - return await this.auditLogService.findAll(filters); - } + // Example: adjust imports/types accordingly + async findAll(@Query() filters: FilterAuditLogDto/* : Promise<PaginatedDto<AuditLogDto>> */) { + return this.auditLogService.findAll(filters); + }Additionally, add @ApiOkResponse with the proper schema.
20-30: Two @query() parameters work but are easy to misuse; consider a composite DTO.Combining filters and groupBy into a single DTO reduces ambiguity and avoids future key collisions.
- async getSummary( - @Query() filters: GroupByFilterDto, - @Query() groupBy: GroupByFieldsDto - ): Promise<AuditLogSummaryDto> { + async getSummary(@Query() query: GroupByFilterDto & GroupByFieldsDto): Promise<AuditLogSummaryDto> { return { - linhas: await this.auditLogService.getSummary(filters, groupBy), + linhas: await this.auditLogService.getSummary(query, query), }; }backend/src/audit-log/entities/audit-log.entity.ts (2)
1-10: Add Swagger property decorators for response models.Without @ApiProperty, Swagger may not render these fields reliably.
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class AuditLogDto { - id: number; - contexto: string; - ip: string; - log: string; - pessoa_id: number | null; - pessoa_nome?: string; // nome_exibicao da pessoa - pessoa_sessao_id: number | null; - criado_em: Date; + @ApiProperty() id: number; + @ApiProperty() contexto: string; + @ApiProperty() ip: string; + @ApiProperty() log: string; + @ApiProperty({ nullable: true }) pessoa_id: number | null; + @ApiPropertyOptional() pessoa_nome?: string; // nome_exibicao da pessoa + @ApiProperty({ nullable: true }) pessoa_sessao_id: number | null; + @ApiProperty({ type: String, format: 'date-time' }) criado_em: Date; }
12-21: Document summary payload in Swagger.Improves consumer UX for the summary endpoint.
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class AuditLogSummaryRow { - count: number; - date?: Date; - contexto?: string; - pessoa_id?: number; + @ApiProperty() count: number; + @ApiPropertyOptional({ type: String, format: 'date-time' }) date?: Date; + @ApiPropertyOptional() contexto?: string; + @ApiPropertyOptional() pessoa_id?: number; } export class AuditLogSummaryDto { - linhas: AuditLogSummaryRow[]; + @ApiProperty({ type: [AuditLogSummaryRow] }) linhas: AuditLogSummaryRow[]; }backend/src/audit-log/dto/audit-log.dto.ts (3)
11-15: Normalize empty strings to undefined for string filters.Matches your number-field handling and avoids filtering by empty strings.
@IsOptional() @IsString() @MaxLength(200) - contexto?: string; + @Transform((a: TransformFnParams) => (a.value === '' ? undefined : a.value)) + contexto?: string;
21-25: Normalize empty strings to undefined for ip.@IsOptional() @IsString() @MaxLength(45) - ip?: string; + @Transform((a: TransformFnParams) => (a.value === '' ? undefined : a.value)) + ip?: string;
34-37: JWT pagination tokens may exceed 1000 chars; raise the cap.Safer headroom for future claims/alg changes.
- @MaxLength(1000) + @MaxLength(2048) token_proxima_pagina?: string;backend/src/audit-log/audit-log.service.ts (4)
99-103: Make contexto filtering consistent with list endpoint (use contains, case-insensitive)Summary uses equality, while list uses contains/insensitive. Align to contains to avoid surprising discrepancies.
- contexto: filters.contexto, + contexto: filters.contexto + ? { contains: filters.contexto, mode: 'insensitive' } + : undefined,
1-1: Use BadRequestException and align error message/param name/languageThe API uses token_proxima_pagina (pt-BR) but the error says “next_page_token” (en). Use BadRequestException and the correct parameter name.
-import { BadRequestException, HttpException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common';- throw new HttpException('Param next_page_token is invalid', 400); + throw new BadRequestException('Parâmetro token_proxima_pagina inválido');Also applies to: 128-128
24-31: Defensive clamp for ipp coming from the tokenEven though the token is server-signed, clamping guards against stale tokens or future config changes exceeding DTO bounds.
const decodedPageToken = this.decodeNextPageToken(filters.token_proxima_pagina); if (decodedPageToken) { offset = decodedPageToken.offset; ipp = decodedPageToken.ipp; } + // Clamp to DTO constraints (1..500) + ipp = Math.min(Math.max(ipp, 1), 500);
45-55: Consider cursor/keyset pagination to avoid page drift under concurrent insertsOffset-based pagination can skip/duplicate rows when new logs arrive between requests. Prefer cursor-based pagination (e.g., by id) for stability.
Example (conceptual):
// orderBy: { id: 'desc' }, cursor: lastId, skip: 1, take: ipp + 1If you must keep created_em primary ordering, implement a composite keyset (criado_em,id) via $queryRaw with a WHERE (criado_em, id) < (:lastDate, :lastId).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
backend/src/app.module.ts(2 hunks)backend/src/audit-log/audit-log.controller.ts(1 hunks)backend/src/audit-log/audit-log.module.ts(1 hunks)backend/src/audit-log/audit-log.service.ts(1 hunks)backend/src/audit-log/dto/audit-log.dto.ts(1 hunks)backend/src/audit-log/entities/audit-log.entity.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/src/audit-log/audit-log.service.ts (2)
backend/src/audit-log/dto/audit-log.dto.ts (3)
FilterAuditLogDto(5-45)GroupByFilterDto(47-47)GroupByFieldsDto(49-61)backend/src/audit-log/entities/audit-log.entity.ts (2)
AuditLogDto(1-10)AuditLogSummaryRow(12-17)
🔇 Additional comments (2)
backend/src/app.module.ts (1)
65-65: Module wiring LGTM.AuditLogModule is correctly imported and registered.
Also applies to: 130-131
backend/src/audit-log/dto/audit-log.dto.ts (1)
47-48: OmitType usage LGTM.Correctly derives GroupByFilterDto from FilterAuditLogDto.
|
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
backend/src/audit-log/audit-log.service.ts (1)
38-43: Fix end-date inclusivity + timezone for criado_em and avoid undefined range in groupBy (repeat from earlier review)Date-only inputs currently exclude most of the end day and groupBy always sends an empty range. Normalize start/end (UTC) and only include the range when provided.
Apply this diff:
@@ - if (filters.criado_em_inicio || filters.criado_em_fim) { - where.criado_em = { - gte: filters.criado_em_inicio ? new Date(filters.criado_em_inicio) : undefined, - lte: filters.criado_em_fim ? new Date(filters.criado_em_fim) : undefined, - }; - } + if (filters.criado_em_inicio || filters.criado_em_fim) { + const range = this.normalizeDateRange(filters.criado_em_inicio, filters.criado_em_fim); + where.criado_em = { gte: range.gte, lte: range.lte }; + } @@ - criado_em: { - gte: filters.criado_em_inicio ? new Date(filters.criado_em_inicio) : undefined, - lte: filters.criado_em_fim ? new Date(filters.criado_em_fim) : undefined, - }, + criado_em: + filters.criado_em_inicio || filters.criado_em_fim + ? this.normalizeDateRange(filters.criado_em_inicio, filters.criado_em_fim) + : undefined,Add this helper inside the service:
private normalizeDateRange(start?: string, end?: string): { gte?: Date; lte?: Date } { const isDateOnly = (s: string) => /^\d{4}-\d{2}-\d{2}$/.test(s); const gte = start ? (isDateOnly(start) ? new Date(`${start}T00:00:00.000Z`) : new Date(start)) : undefined; const lte = end ? (isDateOnly(end) ? new Date(`${end}T23:59:59.999Z`) : new Date(end)) : undefined; return { gte, lte }; }Also applies to: 103-106
🧹 Nitpick comments (7)
backend/src/audit-log/audit-log.service.ts (7)
99-101: Align contexto filter semantics with findAll (use contains/insensitive)GroupBy currently uses equality while list uses contains. Make them consistent.
- pessoa_id: filters.pessoa_id, - contexto: filters.contexto, + pessoa_id: filters.pessoa_id ?? undefined, + contexto: filters.contexto ? { contains: filters.contexto, mode: 'insensitive' } : undefined, log: filters.log_contem ? { contains: filters.log_contem, mode: 'insensitive' } : undefined,
24-31: Clamp ipp/offset from next_page_token to safe boundsJWT is server-signed but hardening prevents unexpected load if values drift. Also clamps query ipp defensively.
- const decodedPageToken = this.decodeNextPageToken(filters.token_proxima_pagina); - if (decodedPageToken) { - offset = decodedPageToken.offset; - ipp = decodedPageToken.ipp; - } + const decodedPageToken = this.decodeNextPageToken(filters.token_proxima_pagina); + if (decodedPageToken) { + offset = Math.max(0, decodedPageToken.offset ?? 0); + ipp = Math.min(500, Math.max(1, decodedPageToken.ipp ?? ipp)); + } + // Final clamp even without token + ipp = Math.min(500, Math.max(1, ipp));
33-37: Type Prisma where input + avoid falsy trap on pessoa_id=0Improves type-safety and prevents accidental omission when id is 0.
- const where: any = {}; - if (filters.pessoa_id) where.pessoa_id = filters.pessoa_id; + const where: Prisma.LogGenericoWhereInput = {}; + if (filters.pessoa_id !== undefined) where.pessoa_id = filters.pessoa_id; if (filters.contexto) where.contexto = { contains: filters.contexto, mode: 'insensitive' }; if (filters.log_contem) where.log = { contains: filters.log_contem, mode: 'insensitive' }; if (filters.ip) where.ip = filters.ip;Add import at top:
import { Prisma } from '@prisma/client';
108-109: Drop no-op orderBy: [] from groupByNot needed and can be removed for clarity.
- orderBy: [],
127-132: Sort non-date groups by count desc for consistencyDate path sorts by count; mirror that on the non-date path.
- return summary.map((r) => ({ - count: r._count._all, - date: undefined, - contexto: r.contexto, - pessoa_id: r.pessoa_id ?? undefined, - })); + return summary + .map((r) => ({ + count: r._count._all, + date: undefined, + contexto: r.contexto, + pessoa_id: r.pessoa_id ?? undefined, + })) + .sort((a, b) => b.count - a.count);
139-141: Use BadRequestException and unify message languageKeeps errors consistent with Nest conventions and Portuguese messages used elsewhere.
- } catch { - throw new HttpException('Param next_page_token is invalid', 400); - } + } catch { + throw new BadRequestException('Parâmetro token_proxima_pagina inválido'); + }
145-147: Align JWT expiration with returned token_ttl in audit-log.service.tsThe JwtModule for pagination in
audit-log.module.tsis configured withexpiresIn: '30d', butPaginatedDto.token_ttl(imported asPAGINATION_TOKEN_TTL) is86400seconds (1 day). To prevent clients from observing a shorter TTL than the actual token lifetime, update theencodeNextPageTokenmethod to sign with{ expiresIn: PAGINATION_TOKEN_TTL }.File:
backend/src/audit-log/audit-log.service.ts(lines 145–147)- private encodeNextPageToken(opt: NextPageTokenJwtBody): string { - return this.jwtService.sign(opt); - } + private encodeNextPageToken(opt: NextPageTokenJwtBody): string { + return this.jwtService.sign(opt, { expiresIn: PAGINATION_TOKEN_TTL }); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
backend/src/audit-log/audit-log.service.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/src/audit-log/audit-log.service.ts (2)
backend/src/audit-log/dto/audit-log.dto.ts (3)
FilterAuditLogDto(5-45)GroupByFilterDto(47-47)GroupByFieldsDto(49-61)backend/src/audit-log/entities/audit-log.entity.ts (2)
AuditLogDto(1-10)AuditLogSummaryRow(12-17)



Summary by CodeRabbit