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>
780 lines
24 KiB
Markdown
780 lines
24 KiB
Markdown
# GoodGo Platform Admin Module & Audit Logging Exploration
|
|
|
|
## Overview
|
|
This document provides a comprehensive analysis of the GoodGo Platform codebase for implementing audit logging in the admin module. The exploration covers the admin module structure, existing patterns, DDD implementation, and event infrastructure.
|
|
|
|
---
|
|
|
|
## 1. ADMIN MODULE STRUCTURE
|
|
|
|
### Directory Layout
|
|
```
|
|
apps/api/src/modules/admin/
|
|
├── admin.module.ts # Module bootstrap & DI configuration
|
|
├── index.ts # Public exports
|
|
│
|
|
├── domain/ # DDD Domain Layer
|
|
│ ├── events/ # Domain events published by commands
|
|
│ │ ├── kyc-approved.event.ts
|
|
│ │ ├── kyc-rejected.event.ts
|
|
│ │ ├── listing-approved.event.ts
|
|
│ │ ├── listing-rejected.event.ts
|
|
│ │ ├── subscription-adjusted.event.ts
|
|
│ │ ├── user-banned.event.ts
|
|
│ │ ├── user-unbanned.event.ts
|
|
│ │ └── index.ts
|
|
│ ├── repositories/
|
|
│ │ ├── admin-query.repository.ts # Query repository interface (read models)
|
|
│ │ └── index.ts
|
|
│ ├── __tests__/
|
|
│ │ └── admin-events.spec.ts
|
|
│ └── index.ts
|
|
│
|
|
├── application/ # CQRS Application Layer
|
|
│ ├── commands/ # Command handlers (mutations)
|
|
│ │ ├── adjust-subscription/
|
|
│ │ │ ├── adjust-subscription.command.ts
|
|
│ │ │ └── adjust-subscription.handler.ts
|
|
│ │ ├── approve-kyc/
|
|
│ │ ├── approve-listing/
|
|
│ │ ├── ban-user/
|
|
│ │ ├── bulk-moderate-listings/
|
|
│ │ ├── reject-kyc/
|
|
│ │ ├── reject-listing/
|
|
│ │ ├── update-user-status/
|
|
│ │ ├── __tests__/ # Each handler has spec
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ ├── queries/ # Query handlers (read models)
|
|
│ │ ├── get-dashboard-stats/
|
|
│ │ ├── get-kyc-queue/
|
|
│ │ ├── get-moderation-queue/
|
|
│ │ ├── get-revenue-stats/
|
|
│ │ ├── get-user-detail/
|
|
│ │ ├── get-users/
|
|
│ │ ├── __tests__/
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ ├── listeners/ # Event subscribers (side effects)
|
|
│ │ ├── user-banned.listener.ts # Deactivates listings, sends notification
|
|
│ │ ├── user-deactivated.listener.ts
|
|
│ │ └── (called via @OnEvent decorator)
|
|
│ │
|
|
│ ├── __tests__/ # Integration tests for handlers
|
|
│ │ └── *.spec.ts files
|
|
│ │
|
|
│ └── index.ts
|
|
│
|
|
├── infrastructure/ # Data Access Layer
|
|
│ ├── repositories/
|
|
│ │ ├── prisma-admin-query.repository.ts # Prisma implementation
|
|
│ │ ├── admin-stats.queries.ts # Raw SQL/Prisma queries
|
|
│ │ ├── admin-user.queries.ts # Raw SQL/Prisma queries
|
|
│ │ └── index.ts
|
|
│ └── index.ts
|
|
│
|
|
└── presentation/ # HTTP Layer
|
|
├── controllers/
|
|
│ ├── admin.controller.ts # User management, subscriptions, dashboard
|
|
│ ├── admin-moderation.controller.ts # Moderation, KYC
|
|
│ └── index.ts
|
|
│
|
|
├── dto/ # Data Transfer Objects
|
|
│ ├── adjust-subscription.dto.ts
|
|
│ ├── approve-kyc.dto.ts
|
|
│ ├── approve-listing.dto.ts
|
|
│ ├── ban-user.dto.ts
|
|
│ ├── bulk-moderate.dto.ts
|
|
│ ├── get-users-query.dto.ts
|
|
│ ├── reject-kyc.dto.ts
|
|
│ ├── reject-listing.dto.ts
|
|
│ ├── revenue-stats.dto.ts
|
|
│ ├── update-user-status.dto.ts
|
|
│ └── index.ts
|
|
│
|
|
└── index.ts
|
|
```
|
|
|
|
---
|
|
|
|
## 2. PRISMA SCHEMA ANALYSIS
|
|
|
|
### Current State
|
|
- **Database**: PostgreSQL 16 + PostGIS
|
|
- **Schema Location**: `prisma/schema.prisma`
|
|
- **Lines**: 602 lines total
|
|
|
|
### User Model (Relevant to Audit)
|
|
```typescript
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String? @unique
|
|
phone String @unique
|
|
passwordHash String?
|
|
fullName String
|
|
avatarUrl String?
|
|
role UserRole @default(BUYER) // BUYER, SELLER, AGENT, ADMIN
|
|
kycStatus KYCStatus @default(NONE) // NONE, PENDING, VERIFIED, REJECTED
|
|
kycData Json?
|
|
isActive Boolean @default(true) // Ban flag
|
|
deletedAt DateTime?
|
|
deletionScheduledAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations...
|
|
@@index([role])
|
|
@@index([kycStatus])
|
|
@@index([isActive])
|
|
@@index([deletedAt])
|
|
@@index([createdAt])
|
|
@@index([role, isActive, createdAt(sort: Desc)])
|
|
@@index([kycStatus, createdAt])
|
|
}
|
|
```
|
|
|
|
### Listing Model (Relevant to Audit)
|
|
```typescript
|
|
model Listing {
|
|
id String @id @default(cuid())
|
|
propertyId String
|
|
agentId String?
|
|
sellerId String
|
|
status ListingStatus @default(DRAFT)
|
|
// DRAFT, PENDING_REVIEW, ACTIVE, RESERVED, SOLD, RENTED, EXPIRED, REJECTED
|
|
moderationScore Float?
|
|
moderationNotes String?
|
|
// ... other fields
|
|
@@index([status])
|
|
@@index([createdAt])
|
|
}
|
|
```
|
|
|
|
### **NO EXISTING AUDIT TABLE**
|
|
- ✅ No AuditLog model found
|
|
- ✅ No AdminAction model found
|
|
- ✅ Opportunity to implement from scratch following project patterns
|
|
|
|
---
|
|
|
|
## 3. ADMIN CONTROLLER ACTIONS & FLOW
|
|
|
|
### AdminController (User Management)
|
|
**File**: `presentation/controllers/admin.controller.ts`
|
|
|
|
#### Endpoints:
|
|
1. **GET /admin/users** - List users with filters
|
|
- Query params: page, limit, role, isActive, search
|
|
- Returns: UserListResult (paginated)
|
|
|
|
2. **GET /admin/users/:id** - Get user details
|
|
- Returns: UserDetail (full profile + activity)
|
|
|
|
3. **PATCH /admin/users/status** - Update user active status
|
|
- Body: `UpdateUserStatusDto` {userId, isActive, reason}
|
|
- Current user (admin) captured via `@CurrentUser()`
|
|
- Command: `UpdateUserStatusCommand(userId, adminId, isActive, reason)`
|
|
|
|
4. **POST /admin/users/ban** - Ban/unban user
|
|
- Body: `BanUserDto` {userId, reason, unban?}
|
|
- Command: `BanUserCommand(userId, adminId, reason, unban)`
|
|
- **Key**: Admin ID is captured from JWT
|
|
|
|
5. **POST /admin/subscriptions/adjust** - Adjust subscription
|
|
- Body: `AdjustSubscriptionDto` {userId, newPlanTier, reason}
|
|
- Command: `AdjustSubscriptionCommand(userId, adminId, newPlanTier, reason)`
|
|
|
|
6. **GET /admin/dashboard** - Dashboard stats
|
|
- Query: `GetDashboardStatsQuery`
|
|
|
|
7. **GET /admin/revenue** - Revenue statistics
|
|
- Query params: startDate, endDate, groupBy (day/month)
|
|
|
|
### AdminModerationController (Content Moderation)
|
|
**File**: `presentation/controllers/admin-moderation.controller.ts`
|
|
|
|
#### Endpoints:
|
|
1. **GET /admin/moderation** - Get moderation queue
|
|
- Query params: page, limit
|
|
- Returns: ModerationQueueResult
|
|
|
|
2. **POST /admin/moderation/approve** - Approve listing
|
|
- Body: `ApproveListingDto` {listingId, moderationNotes}
|
|
- Command: `ApproveListingCommand(listingId, adminId, moderationNotes)`
|
|
- Event: `ListingApprovedEvent` published
|
|
|
|
3. **POST /admin/moderation/reject** - Reject listing
|
|
- Body: `RejectListingDto` {listingId, reason}
|
|
- Command: `RejectListingCommand(listingId, adminId, reason)`
|
|
- Event: `ListingRejectedEvent` published
|
|
|
|
4. **POST /admin/moderation/bulk** - Bulk moderate
|
|
- Body: `BulkModerateDto` {listingIds[], action, reason}
|
|
- Command: `BulkModerateListingsCommand(...)`
|
|
|
|
5. **GET /admin/kyc** - Get KYC queue
|
|
- Returns: KycQueueResult (users with PENDING KYC)
|
|
|
|
6. **POST /admin/kyc/approve** - Approve KYC
|
|
- Body: `ApproveKycDto` {userId, comments}
|
|
- Command: `ApproveKycCommand(userId, adminId, comments)`
|
|
- Event: `KycApprovedEvent` published
|
|
|
|
7. **POST /admin/kyc/reject** - Reject KYC
|
|
- Body: `RejectKycDto` {userId, reason}
|
|
- Command: `RejectKycCommand(userId, adminId, reason)`
|
|
- Event: `KycRejectedEvent` published
|
|
|
|
### Key Pattern: Admin ID Capture
|
|
```typescript
|
|
@Post('moderation/approve')
|
|
async approveListing(
|
|
@Body() dto: ApproveListingDto,
|
|
@CurrentUser() user: JwtPayload, // ← Admin's identity
|
|
) {
|
|
return this.commandBus.execute(
|
|
new ApproveListingCommand(dto.listingId, user.sub, dto.moderationNotes)
|
|
);
|
|
// user.sub = admin's userId
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. EXISTING EVENT/LOGGING INFRASTRUCTURE
|
|
|
|
### DomainEvent Interface
|
|
**Location**: `@modules/shared`
|
|
|
|
```typescript
|
|
// All domain events implement DomainEvent:
|
|
export interface DomainEvent {
|
|
readonly eventName: string;
|
|
readonly occurredAt: Date;
|
|
readonly aggregateId: string; // What changed (user/listing ID)
|
|
}
|
|
```
|
|
|
|
### Example: UserBannedEvent
|
|
```typescript
|
|
export class UserBannedEvent implements DomainEvent {
|
|
readonly eventName = 'user.banned';
|
|
readonly occurredAt = new Date();
|
|
|
|
constructor(
|
|
public readonly aggregateId: string, // userId
|
|
public readonly adminId: string, // ← Admin who performed action
|
|
public readonly reason: string,
|
|
) {}
|
|
}
|
|
```
|
|
|
|
### Example: ListingApprovedEvent
|
|
```typescript
|
|
export class ListingApprovedEvent implements DomainEvent {
|
|
readonly eventName = 'listing.approved';
|
|
readonly occurredAt = new Date();
|
|
|
|
constructor(
|
|
public readonly aggregateId: string, // listingId
|
|
public readonly adminId: string, // ← Admin who approved
|
|
public readonly moderationNotes?: string,
|
|
) {}
|
|
}
|
|
```
|
|
|
|
### Event Publishing & Listening
|
|
**Pattern Used**: NestJS CQRS + EventEmitter
|
|
|
|
#### Publishing (in Command Handlers):
|
|
```typescript
|
|
@CommandHandler(BanUserCommand)
|
|
export class BanUserHandler implements ICommandHandler<BanUserCommand> {
|
|
constructor(
|
|
private readonly userRepo: IUserRepository,
|
|
private readonly eventBus: EventBus, // ← Injected
|
|
) {}
|
|
|
|
async execute(command: BanUserCommand): Promise<BanUserResult> {
|
|
// ... business logic ...
|
|
|
|
this.eventBus.publish(
|
|
new UserBannedEvent(user.id, command.adminId, command.reason)
|
|
);
|
|
|
|
return { userId: user.id, isActive: false, message: 'Người dùng đã bị ban' };
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Listening (in Event Listeners):
|
|
```typescript
|
|
@Injectable()
|
|
export class UserBannedListener {
|
|
constructor(
|
|
private readonly commandBus: CommandBus,
|
|
private readonly prisma: PrismaService,
|
|
private readonly logger: LoggerService,
|
|
) {}
|
|
|
|
@OnEvent('user.banned', { async: true })
|
|
async handle(event: UserBannedEvent): Promise<void> {
|
|
this.logger.log(
|
|
`Handling user.banned for user ${event.aggregateId}`,
|
|
'UserBannedListener'
|
|
);
|
|
|
|
// Side effects: deactivate listings, send notification, etc.
|
|
const deactivated = await this.prisma.listing.updateMany({
|
|
where: { sellerId: event.aggregateId, status: { in: ['ACTIVE', ...] } },
|
|
data: { status: 'EXPIRED' },
|
|
});
|
|
|
|
// Send email notification
|
|
await this.commandBus.execute(
|
|
new SendNotificationCommand(user.id, 'EMAIL', 'user.banned', ...)
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### EventBus Architecture
|
|
- **Module**: `@nestjs/cqrs`
|
|
- **Setup**: `CqrsModule.forRoot()` in `app.module.ts`
|
|
- **Mechanism**:
|
|
- Commands publish events via `eventBus.publish(event)`
|
|
- Listeners subscribe via `@OnEvent(eventName, { async: true })`
|
|
- Events are async by default (non-blocking)
|
|
|
|
---
|
|
|
|
## 5. LOGGER SERVICE
|
|
|
|
**Location**: `apps/api/src/modules/shared/infrastructure/logger.service.ts`
|
|
|
|
### Features
|
|
- **Provider**: Pino (structured logging)
|
|
- **PII Redaction**: Automatic masking of sensitive fields
|
|
- Redacted paths: password, token, email, phone, kycData, creditCard, etc.
|
|
- Censor pattern: `[REDACTED]`
|
|
- **Environment-aware**:
|
|
- Dev: Pretty-printed with colors
|
|
- Prod: Structured JSON
|
|
- **Methods**:
|
|
- `log(message, context)`
|
|
- `error(message, trace, context)`
|
|
- `warn(message, context)`
|
|
- `debug(message, context)`
|
|
- `verbose(message, context)`
|
|
- `child(bindings)` - Child logger with context binding
|
|
|
|
### Redacted Fields
|
|
```typescript
|
|
redact: {
|
|
paths: [
|
|
'password', 'passwordHash', 'token', 'accessToken', 'refreshToken',
|
|
'secret', 'authorization', 'cookie', 'creditCard', 'cardNumber',
|
|
'cvv', 'ssn', 'cmnd', 'cccd', 'email', 'phone', 'kycData',
|
|
'idNumber', 'identityNumber', 'dateOfBirth', 'dob', 'address',
|
|
'bankAccount', 'accountNumber', 'apiKey', 'privateKey', 'encryptionKey',
|
|
'req.headers.authorization', 'req.headers.cookie',
|
|
'user.email', 'user.phone', 'user.kycData',
|
|
'body.password', 'body.token', 'body.email', 'body.phone',
|
|
],
|
|
censor: '[REDACTED]',
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. EXCEPTION HANDLING & FILTERS
|
|
|
|
### GlobalExceptionFilter
|
|
**Location**: `apps/api/src/modules/shared/infrastructure/filters/global-exception.filter.ts`
|
|
|
|
```typescript
|
|
@Catch()
|
|
export class GlobalExceptionFilter implements ExceptionFilter {
|
|
constructor(private readonly logger: LoggerService) {}
|
|
|
|
catch(exception: unknown, host: ArgumentsHost): void {
|
|
const ctx = host.switchToHttp();
|
|
const response = ctx.getResponse<Response>();
|
|
const request = ctx.getRequest<Request>();
|
|
const correlationId = (request.headers['x-correlation-id'] as string) ?? undefined;
|
|
|
|
const errorResponse = this.buildErrorResponse(exception, correlationId);
|
|
|
|
this.logger.error(
|
|
`[${errorResponse.errorCode}] ${errorResponse.message}`,
|
|
exception instanceof Error ? exception.stack : undefined,
|
|
'GlobalExceptionFilter'
|
|
);
|
|
|
|
response.status(errorResponse.statusCode).json(errorResponse);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Response Structure
|
|
```typescript
|
|
interface ErrorResponseBody {
|
|
statusCode: number;
|
|
errorCode: ErrorCode; // Enum with values like VALIDATION_FAILED, NOT_FOUND, etc.
|
|
message: string;
|
|
details?: Record<string, unknown>;
|
|
correlationId?: string;
|
|
timestamp: string;
|
|
}
|
|
```
|
|
|
|
### Domain Exceptions
|
|
```typescript
|
|
export class DomainException extends HttpException {
|
|
constructor(
|
|
public readonly errorCode: ErrorCode,
|
|
message: string,
|
|
statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
|
|
public readonly details?: Record<string, unknown>,
|
|
) {
|
|
super(message, statusCode);
|
|
}
|
|
}
|
|
|
|
// Specific exceptions:
|
|
export class NotFoundException extends DomainException { ... }
|
|
export class ValidationException extends DomainException { ... }
|
|
export class ConflictException extends DomainException { ... }
|
|
export class UnauthorizedException extends DomainException { ... }
|
|
export class ForbiddenException extends DomainException { ... }
|
|
```
|
|
|
|
---
|
|
|
|
## 7. SECURITY & GUARDS
|
|
|
|
### Role-Based Access Control (RBAC)
|
|
**Location**: `apps/api/src/modules/auth/presentation/decorators/`
|
|
|
|
#### Pattern:
|
|
```typescript
|
|
@Controller('admin')
|
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
@Roles('ADMIN')
|
|
export class AdminController {
|
|
// All endpoints require ADMIN role
|
|
}
|
|
```
|
|
|
|
#### Roles Guard Flow:
|
|
1. `JwtAuthGuard` - Validates JWT token
|
|
2. `RolesGuard` - Checks `@Roles()` decorator against user.role
|
|
3. Both decorators from `@modules/auth`
|
|
|
|
### Rate Limiting
|
|
**Setup**: ThrottlerModule + ThrottlerBehindProxyGuard
|
|
- Default: 60 requests per 60 seconds per IP
|
|
- Auth endpoints: 10 requests per 60 seconds
|
|
- Payment callbacks: 20 requests per 60 seconds
|
|
|
|
---
|
|
|
|
## 8. DDD LAYER STRUCTURE
|
|
|
|
### Architectural Layers
|
|
|
|
```
|
|
Presentation Layer (Controllers)
|
|
↓ (DTO validation)
|
|
Application Layer (Commands/Queries/Handlers/Listeners)
|
|
↓ (Command/Query)
|
|
Domain Layer (Events, Interfaces, Business Rules)
|
|
↓ (Repository calls, Event publishing)
|
|
Infrastructure Layer (Prisma, Database)
|
|
```
|
|
|
|
### Command Handler Pattern
|
|
```typescript
|
|
// 1. Command (DTO-like)
|
|
export class BanUserCommand {
|
|
constructor(
|
|
public readonly userId: string,
|
|
public readonly adminId: string,
|
|
public readonly reason: string,
|
|
public readonly unban: boolean = false,
|
|
) {}
|
|
}
|
|
|
|
// 2. Handler (Business Logic + Event Publishing)
|
|
@CommandHandler(BanUserCommand)
|
|
export class BanUserHandler implements ICommandHandler<BanUserCommand> {
|
|
constructor(
|
|
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
|
private readonly eventBus: EventBus,
|
|
) {}
|
|
|
|
async execute(command: BanUserCommand): Promise<BanUserResult> {
|
|
// Business logic
|
|
const user = await this.userRepo.findById(command.userId);
|
|
if (!user) throw new NotFoundException(...);
|
|
|
|
user.deactivate();
|
|
await this.userRepo.update(user);
|
|
|
|
// Publish event
|
|
this.eventBus.publish(
|
|
new UserBannedEvent(user.id, command.adminId, command.reason)
|
|
);
|
|
|
|
return { userId: user.id, isActive: false, message: '...' };
|
|
}
|
|
}
|
|
|
|
// 3. Event (Side-effect Trigger)
|
|
export class UserBannedEvent implements DomainEvent {
|
|
readonly eventName = 'user.banned';
|
|
readonly occurredAt = new Date();
|
|
|
|
constructor(
|
|
public readonly aggregateId: string,
|
|
public readonly adminId: string,
|
|
public readonly reason: string,
|
|
) {}
|
|
}
|
|
|
|
// 4. Listener (Side Effects - triggered by Event)
|
|
@Injectable()
|
|
export class UserBannedListener {
|
|
@OnEvent('user.banned', { async: true })
|
|
async handle(event: UserBannedEvent): Promise<void> {
|
|
// Send notification, update related data, etc.
|
|
}
|
|
}
|
|
```
|
|
|
|
### Query Handler Pattern
|
|
```typescript
|
|
// Query (read operation definition)
|
|
export class GetDashboardStatsQuery {}
|
|
|
|
// Handler (fetch & return data)
|
|
@QueryHandler(GetDashboardStatsQuery)
|
|
export class GetDashboardStatsHandler implements IQueryHandler<GetDashboardStatsQuery> {
|
|
constructor(
|
|
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
|
|
) {}
|
|
|
|
async execute(_query: GetDashboardStatsQuery): Promise<DashboardStats> {
|
|
return this.adminQueryRepo.getDashboardStats();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Repository Pattern
|
|
```typescript
|
|
// Domain interface (no implementation details)
|
|
export interface IAdminQueryRepository {
|
|
getModerationQueue(page: number, limit: number): Promise<ModerationQueueResult>;
|
|
getDashboardStats(): Promise<DashboardStats>;
|
|
// ... more methods
|
|
}
|
|
|
|
// Infrastructure implementation (Prisma-specific)
|
|
@Injectable()
|
|
export class PrismaAdminQueryRepository implements IAdminQueryRepository {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async getModerationQueue(page: number, limit: number): Promise<ModerationQueueResult> {
|
|
// Prisma queries here
|
|
}
|
|
}
|
|
|
|
// DI Token
|
|
export const ADMIN_QUERY_REPOSITORY = Symbol('ADMIN_QUERY_REPOSITORY');
|
|
|
|
// Module registration
|
|
@Module({
|
|
providers: [
|
|
{ provide: ADMIN_QUERY_REPOSITORY, useClass: PrismaAdminQueryRepository },
|
|
],
|
|
})
|
|
export class AdminModule {}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. MODULE BOOTSTRAP
|
|
|
|
### AdminModule Setup
|
|
**File**: `apps/api/src/modules/admin/admin.module.ts`
|
|
|
|
```typescript
|
|
@Module({
|
|
imports: [CqrsModule, AuthModule, ListingsModule, SubscriptionsModule],
|
|
controllers: [AdminController, AdminModerationController],
|
|
providers: [
|
|
// Repositories
|
|
{ provide: ADMIN_QUERY_REPOSITORY, useClass: PrismaAdminQueryRepository },
|
|
|
|
// CQRS Handlers
|
|
...CommandHandlers, // 8 command handlers
|
|
...QueryHandlers, // 6 query handlers
|
|
|
|
// Event Listeners
|
|
UserBannedListener,
|
|
UserDeactivatedListener,
|
|
],
|
|
})
|
|
export class AdminModule {}
|
|
```
|
|
|
|
### Global Setup
|
|
**File**: `apps/api/src/app.module.ts`
|
|
|
|
```typescript
|
|
@Module({
|
|
imports: [
|
|
SentryModule.forRoot(),
|
|
CqrsModule.forRoot(), // ← CQRS with Event Bus
|
|
ScheduleModule.forRoot(),
|
|
ThrottlerModule.forRoot(...), // ← Rate limiting
|
|
// ... other modules including AdminModule
|
|
],
|
|
providers: [
|
|
{
|
|
provide: APP_FILTER,
|
|
useClass: SentryGlobalFilter,
|
|
},
|
|
{
|
|
provide: APP_GUARD,
|
|
useClass: ThrottlerBehindProxyGuard,
|
|
},
|
|
{
|
|
provide: APP_INTERCEPTOR,
|
|
useClass: HttpMetricsInterceptor,
|
|
},
|
|
],
|
|
})
|
|
export class AppModule implements NestModule {
|
|
configure(consumer: MiddlewareConsumer) {
|
|
consumer.apply(SanitizeInputMiddleware).forRoutes('*');
|
|
consumer.apply(CsrfMiddleware).exclude(...).forRoutes('*');
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 10. EXISTING INTERCEPTOR PATTERNS
|
|
|
|
### HttpMetricsInterceptor
|
|
**Location**: `@modules/metrics`
|
|
|
|
- Injected globally via `APP_INTERCEPTOR`
|
|
- Tracks HTTP request/response metrics
|
|
- Could serve as a template for audit logging interceptor
|
|
|
|
### CSRF Middleware
|
|
**Location**: `@modules/shared/infrastructure/middleware/csrf.middleware`
|
|
|
|
- Double-submit cookie pattern
|
|
- Validates on state-changing methods
|
|
- Provides model for request context enhancement
|
|
|
|
---
|
|
|
|
## 11. SUMMARY FOR AUDIT LOGGING IMPLEMENTATION
|
|
|
|
### What's Already in Place ✅
|
|
1. **Event-driven architecture** - Commands publish events, listeners handle side effects
|
|
2. **Admin identity capture** - All admin actions have `adminId` in commands
|
|
3. **Logger service** - Pino-based with PII redaction
|
|
4. **Exception handling** - Global filter + DomainException hierarchy
|
|
5. **RBAC** - @Roles('ADMIN') guard in place
|
|
6. **Module bootstrap** - Clear DI pattern ready for audit repository injection
|
|
7. **DTO validation** - All inputs validated via class-validator
|
|
|
|
### What Needs to Be Built 🚀
|
|
1. **AuditLog Prisma Model** - Store in database
|
|
2. **AuditLoggingInterceptor** - Capture HTTP context (IP, timestamp, endpoint)
|
|
3. **AuditEvent Domain Event** - Extend domain events for audit purposes
|
|
4. **AuditLoggingListener** - Event listener that persists to AuditLog
|
|
5. **AuditLog Repository** - CRUD operations for AuditLog
|
|
6. **Query Handler** - Retrieve audit logs with filters (date range, admin, action type)
|
|
7. **Controller Endpoint** - GET /admin/audit-logs for viewing audit trail
|
|
|
|
### Key Integration Points
|
|
1. Commands already pass `adminId` → Use in AuditLoggingListener
|
|
2. Domain events already published → Hook audit listener to relevant events
|
|
3. HTTPContext (IP, user-agent, etc.) → Capture in interceptor
|
|
4. Logger service available → Use for structured logging
|
|
5. Repository pattern established → Follow for AuditLog repository
|
|
|
|
### Recommended Audit Fields
|
|
- `id` (primary key)
|
|
- `adminId` (who performed action)
|
|
- `adminName` (for denormalization)
|
|
- `action` (e.g., 'user.banned', 'listing.approved')
|
|
- `resourceType` (e.g., 'user', 'listing')
|
|
- `resourceId` (what was affected)
|
|
- `changes` (JSON of what changed - before/after)
|
|
- `reason` (from DTO if provided)
|
|
- `ipAddress` (from request)
|
|
- `userAgent` (from request)
|
|
- `status` (success/failure)
|
|
- `statusCode` (HTTP status)
|
|
- `errorMessage` (if failed)
|
|
- `duration` (milliseconds)
|
|
- `createdAt` (timestamp)
|
|
|
|
---
|
|
|
|
## Key Files Reference
|
|
|
|
### Controllers
|
|
- `presentation/controllers/admin.controller.ts` - Main admin operations
|
|
- `presentation/controllers/admin-moderation.controller.ts` - Moderation & KYC
|
|
|
|
### Command Handlers (Action Points)
|
|
- `application/commands/ban-user/ban-user.handler.ts`
|
|
- `application/commands/approve-listing/approve-listing.handler.ts`
|
|
- `application/commands/approve-kyc/approve-kyc.handler.ts`
|
|
- `application/commands/reject-listing/reject-listing.handler.ts`
|
|
- `application/commands/reject-kyc/reject-kyc.handler.ts`
|
|
- `application/commands/adjust-subscription/adjust-subscription.handler.ts`
|
|
- `application/commands/update-user-status/update-user-status.handler.ts`
|
|
- `application/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts`
|
|
|
|
### Event Listeners (Where to Hook Audit)
|
|
- `application/listeners/user-banned.listener.ts`
|
|
- `application/listeners/user-deactivated.listener.ts`
|
|
|
|
### Infrastructure
|
|
- `infrastructure/repositories/prisma-admin-query.repository.ts`
|
|
- `infrastructure/repositories/admin-stats.queries.ts`
|
|
- `infrastructure/repositories/admin-user.queries.ts`
|
|
|
|
### Shared Resources
|
|
- Logger: `@modules/shared/infrastructure/logger.service.ts`
|
|
- Exception Filter: `@modules/shared/infrastructure/filters/global-exception.filter.ts`
|
|
- Roles Guard: `@modules/auth` (decorators + guard)
|
|
|
|
### Prisma
|
|
- Schema: `prisma/schema.prisma` (602 lines, no audit model yet)
|
|
- Client type safety guaranteed
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. ✅ **Schema Design** - Create AuditLog model in Prisma
|
|
2. ✅ **Repository Pattern** - Create AuditLogRepository with interface
|
|
3. ✅ **Audit Events** - Create domain events for audit purposes
|
|
4. ✅ **Event Listener** - Create AuditLoggingListener to persist events
|
|
5. ✅ **Interceptor** - Capture HTTP context (optional enhancement)
|
|
6. ✅ **Query Handler** - Create query/handler for retrieving audit logs
|
|
7. ✅ **Controller Endpoint** - Add GET /admin/audit-logs endpoint
|
|
8. ✅ **Tests** - Unit and integration tests
|
|
9. ✅ **Documentation** - API docs in Swagger
|
|
|