Root directory had accumulated audit/exploration markdown files cluttering the project root. Moved all audit-related files to docs/audits/ with a README.md index, and updated cross-references in K6_LOAD_TESTING_GUIDE.md and README_FRONTEND_DOCS.md. Co-Authored-By: Paperclip <noreply@paperclip.ing>
9.0 KiB
9.0 KiB
Quick File Reference for Admin Module Audit Logging
MUST READ FIRST (15 min total)
1. Main Controllers (Define what actions need audit)
-
✅
apps/api/src/modules/admin/presentation/controllers/admin.controller.ts(155 lines)- User management: ban, update status, adjust subscription
- All endpoints have @CurrentUser() decorator to capture admin ID
-
✅
apps/api/src/modules/admin/presentation/controllers/admin-moderation.controller.ts(157 lines)- Listing approval/rejection
- KYC approval/rejection
- Bulk moderation
2. Command Handlers (Where to hook audit logging)
Each command publishes a domain event. Audit logging listener should listen to these events.
Ban User Flow:
- Input:
apps/api/src/modules/admin/presentation/dto/ban-user.dto.ts - Command:
apps/api/src/modules/admin/application/commands/ban-user/ban-user.command.ts - Handler:
apps/api/src/modules/admin/application/commands/ban-user/ban-user.handler.ts(70 lines)- Line 62:
this.eventBus.publish(new UserBannedEvent(...))
- Line 62:
Approve Listing Flow:
- Input:
apps/api/src/modules/admin/presentation/dto/approve-listing.dto.ts - Command:
apps/api/src/modules/admin/application/commands/approve-listing/approve-listing.command.ts - Handler:
apps/api/src/modules/admin/application/commands/approve-listing/approve-listing.handler.ts(52 lines)- Line 42-44:
this.eventBus.publish(new ListingApprovedEvent(...))
- Line 42-44:
3. Domain Events (What information is published)
apps/api/src/modules/admin/domain/events/
├── user-banned.event.ts
├── user-unbanned.event.ts
├── listing-approved.event.ts
├── listing-rejected.event.ts
├── subscription-adjusted.event.ts
├── kyc-approved.event.ts
└── kyc-rejected.event.ts
Each event has:
eventName(e.g., 'user.banned')occurredAt(timestamp)aggregateId(userId or listingId)adminId(the admin who performed the action)- Additional context (reason, notes, etc.)
4. Existing Event Listener Pattern (Template)
- ✅
apps/api/src/modules/admin/application/listeners/user-banned.listener.ts(52 lines)- Shows @OnEvent() decorator
- Shows how to access event data
- Shows side effects (deactivate listings, send notification)
- THIS IS YOUR TEMPLATE FOR AUDIT LOGGING LISTENER
5. Logger Service (Where to log)
- ✅
apps/api/src/modules/shared/infrastructure/logger.service.ts(65 lines)- Pino-based structured logging
- Auto PII redaction
- Methods: log(), error(), warn(), debug(), verbose()
6. Exception Filter (For error logging)
- ✅
apps/api/src/modules/shared/infrastructure/filters/global-exception.filter.ts(145 lines)- Catches all exceptions
- Logs with correlationId
- Could capture failed admin actions
ARCHITECTURE REFERENCES
Repository Pattern
- Domain Interface:
apps/api/src/modules/admin/domain/repositories/admin-query.repository.ts - Prisma Implementation:
apps/api/src/modules/admin/infrastructure/repositories/prisma-admin-query.repository.ts - FOLLOW THIS PATTERN for AuditLog repository
Module Bootstrap
apps/api/src/modules/admin/admin.module.ts(64 lines)- Shows how to register repositories via DI
- Shows how to register listeners
- Shows how to import CQRS module
Global App Setup
apps/api/src/app.module.ts(100+ lines)- Shows APP_FILTER, APP_GUARD, APP_INTERCEPTOR registration
- Shows CqrsModule.forRoot() setup
- Shows middleware configuration
PRISMA SCHEMA
Current Models (What we're auditing)
prisma/schema.prisma(602 lines total)
User Model (lines 34-71):
- Fields to audit: isActive, kycStatus, role
Listing Model (lines 227-276):
- Fields to audit: status, moderationScore, moderationNotes
NO AUDIT MODEL YET - Opportunity to create from scratch
EXACT ENDPOINTS TO AUDIT (From Controllers)
AdminController Actions:
PATCH /admin/users/status- Update user active statusPOST /admin/users/ban- Ban/unban userPOST /admin/subscriptions/adjust- Adjust subscription
AdminModerationController Actions:
POST /admin/moderation/approve- Approve listingPOST /admin/moderation/reject- Reject listingPOST /admin/moderation/bulk- Bulk moderate listingsPOST /admin/kyc/approve- Approve KYCPOST /admin/kyc/reject- Reject KYC
Each action:
- Already captures admin ID from JWT
- Already publishes a domain event
- Already has a command handler
- Needs: Audit logging listener to capture to database
DEPENDENCIES ALREADY IMPORTED
In AdminModule:
// Already available:
- CqrsModule (from @nestjs/cqrs)
- AuthModule (auth guards/decorators)
- ListingsModule (for listing operations)
- SubscriptionsModule (for subscription operations)
// In providers:
- CommandHandlers (8 total)
- QueryHandlers (6 total)
- Event Listeners (2 existing + need to add AuditLoggingListener)
From SharedModule:
PrismaService(database)LoggerService(logging)- Exception types (NotFoundException, ValidationException, etc.)
IMPLEMENTATION CHECKLIST
Phase 1: Database & Repository
- Create AuditLog Prisma model in schema.prisma
- Create IAuditLogRepository interface
- Create PrismaAuditLogRepository implementation
- Add to AdminModule providers
Phase 2: Events & Listeners
- Create AuditEvent domain event (if needed as wrapper)
- Create AuditLoggingListener to @OnEvent() for all admin events
- Inject AuditLogRepository into listener
- Persist audit records on event
Phase 3: Query & API
- Create GetAuditLogsQuery
- Create GetAuditLogsHandler
- Create IAuditLogQueryRepository method
- Add to QueryHandlers in module
Phase 4: Controller Endpoint
- Add GET /admin/audit-logs endpoint
- Add filtering DTOs (dateRange, adminId, actionType, resourceId)
- Add pagination support
Phase 5: Testing
- Unit tests for AuditLoggingListener
- Integration tests for audit persistence
- E2E tests for audit log retrieval
CRITICAL PATTERNS TO FOLLOW
1. Command Pattern (Already Used)
// DTOs validate input
// Commands encapsulate business intent
// Handlers execute + publish events
// Events trigger side effects via listeners
2. DDD Layer Structure
Presentation (DTO validation)
↓
Application (Command/Query execution + Event publishing)
↓
Domain (Event definitions, Repository interfaces)
↓
Infrastructure (Database implementation)
3. Dependency Injection
// Always use Symbol for tokens:
export const AUDIT_LOG_REPOSITORY = Symbol('AUDIT_LOG_REPOSITORY');
// Register in module:
{ provide: AUDIT_LOG_REPOSITORY, useClass: PrismaAuditLogRepository }
// Inject in service:
constructor(
@Inject(AUDIT_LOG_REPOSITORY) private readonly auditRepo: IAuditLogRepository,
) {}
4. Event Listener Pattern
@Injectable()
export class AuditLoggingListener {
constructor(
@Inject(AUDIT_LOG_REPOSITORY) private readonly auditRepo: IAuditLogRepository,
private readonly logger: LoggerService,
) {}
@OnEvent('user.banned', { async: true })
async handleUserBanned(event: UserBannedEvent): Promise<void> {
// Extract data from event
// Persist to database
// Log if successful/failed
}
}
WHERE TO ADD CODE
apps/api/src/modules/admin/
├── domain/
│ ├── events/
│ │ └── audit-logged.event.ts (NEW - optional wrapper)
│ └── repositories/
│ ├── audit-log.repository.ts (NEW - interface)
│ └── index.ts (update exports)
│
├── application/
│ ├── queries/
│ │ ├── get-audit-logs/ (NEW)
│ │ │ ├── get-audit-logs.query.ts
│ │ │ └── get-audit-logs.handler.ts
│ │ └── index.ts (update exports)
│ │
│ └── listeners/
│ ├── audit-logging.listener.ts (NEW)
│ └── index.ts (update if needed)
│
├── infrastructure/
│ └── repositories/
│ ├── prisma-audit-log.repository.ts (NEW)
│ └── index.ts (update exports)
│
└── presentation/
├── controllers/
│ ├── admin.controller.ts (ADD ENDPOINT)
│ └── admin-moderation.controller.ts (UPDATE if needed)
│
└── dto/
├── get-audit-logs-query.dto.ts (NEW)
└── index.ts (update exports)
prisma/
└── schema.prisma (ADD AuditLog MODEL)
EVENTS TO LISTEN TO
- 'user.banned' - from UserBannedEvent
- 'user.unbanned' - from UserUnbannedEvent
- 'listing.approved' - from ListingApprovedEvent
- 'listing.rejected' - from ListingRejectedEvent
- 'kyc.approved' - from KycApprovedEvent
- 'kyc.rejected' - from KycRejectedEvent
- 'subscription.adjusted' - from SubscriptionAdjustedEvent
- 'user.deactivated' - (if exists in auth module)
Each gets logged with:
- Admin ID (from event)
- Resource ID (aggregateId from event)
- Resource Type (derived from eventName)
- Timestamp (from event.occurredAt)
- Additional context (reason, notes, etc.)