# Khám Phá Mô-đun Admin & Ghi Log Kiểm Toán Nền Tảng GoodGo ## Tổng Quan Tài liệu này cung cấp phân tích toàn diện về codebase Nền Tảng GoodGo nhằm mục đích triển khai tính năng ghi log kiểm toán trong mô-đun admin. Quá trình khám phá bao gồm cấu trúc mô-đun admin, các mẫu hiện có, triển khai DDD và cơ sở hạ tầng sự kiện. --- ## 1. CẤU TRÚC MÔ-ĐUN ADMIN ### Cấu Trúc Thư Mục ``` apps/api/src/modules/admin/ ├── admin.module.ts # Bootstrap mô-đun & cấu hình DI ├── index.ts # Xuất công khai │ ├── domain/ # Tầng Domain DDD │ ├── events/ # Sự kiện domain được phát hành bởi lệnh │ │ ├── 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 # Giao diện repository truy vấn (read models) │ │ └── index.ts │ ├── __tests__/ │ │ └── admin-events.spec.ts │ └── index.ts │ ├── application/ # Tầng Ứng Dụng CQRS │ ├── commands/ # Bộ xử lý lệnh (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__/ # Mỗi handler có spec riêng │ │ └── index.ts │ │ │ ├── queries/ # Bộ xử lý truy vấn (read models) │ │ ├── get-dashboard-stats/ │ │ ├── get-kyc-queue/ │ │ ├── get-moderation-queue/ │ │ ├── get-revenue-stats/ │ │ ├── get-user-detail/ │ │ ├── get-users/ │ │ ├── __tests__/ │ │ └── index.ts │ │ │ ├── listeners/ # Người đăng ký sự kiện (hiệu ứng phụ) │ │ ├── user-banned.listener.ts # Vô hiệu hoá listing, gửi thông báo │ │ ├── user-deactivated.listener.ts │ │ └── (gọi qua decorator @OnEvent) │ │ │ ├── __tests__/ # Kiểm thử tích hợp cho các handler │ │ └── *.spec.ts files │ │ │ └── index.ts │ ├── infrastructure/ # Tầng Truy Cập Dữ Liệu │ ├── repositories/ │ │ ├── prisma-admin-query.repository.ts # Triển khai Prisma │ │ ├── admin-stats.queries.ts # Truy vấn SQL thuần/Prisma │ │ ├── admin-user.queries.ts # Truy vấn SQL thuần/Prisma │ │ └── index.ts │ └── index.ts │ └── presentation/ # Tầng HTTP ├── controllers/ │ ├── admin.controller.ts # Quản lý người dùng, gói đăng ký, dashboard │ ├── admin-moderation.controller.ts # Kiểm duyệt, KYC │ └── index.ts │ ├── dto/ # Đối Tượng Truyền Dữ Liệu │ ├── 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. PHÂN TÍCH SCHEMA PRISMA ### Trạng Thái Hiện Tại - **Cơ sở dữ liệu**: PostgreSQL 16 + PostGIS - **Vị trí Schema**: `prisma/schema.prisma` - **Số dòng**: Tổng cộng 602 dòng ### Model User (Liên Quan Đến Kiểm Toán) ```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) // Cờ ban 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]) } ``` ### Model Listing (Liên Quan Đến Kiểm Toán) ```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]) } ``` ### **CHƯA CÓ BẢNG KIỂM TOÁN** - ✅ Không tìm thấy model AuditLog - ✅ Không tìm thấy model AdminAction - ✅ Cơ hội triển khai từ đầu theo các mẫu của dự án --- ## 3. LUỒNG & HÀNH ĐỘNG ADMIN CONTROLLER ### AdminController (Quản Lý Người Dùng) **File**: `presentation/controllers/admin.controller.ts` #### Các Endpoint: 1. **GET /admin/users** - Danh sách người dùng với bộ lọc - Tham số truy vấn: page, limit, role, isActive, search - Trả về: UserListResult (có phân trang) 2. **GET /admin/users/:id** - Lấy chi tiết người dùng - Trả về: UserDetail (thông tin đầy đủ + hoạt động) 3. **PATCH /admin/users/status** - Cập nhật trạng thái hoạt động của người dùng - Body: `UpdateUserStatusDto` {userId, isActive, reason} - Người dùng hiện tại (admin) được lấy qua `@CurrentUser()` - Lệnh: `UpdateUserStatusCommand(userId, adminId, isActive, reason)` 4. **POST /admin/users/ban** - Cấm/bỏ cấm người dùng - Body: `BanUserDto` {userId, reason, unban?} - Lệnh: `BanUserCommand(userId, adminId, reason, unban)` - **Chú ý**: Admin ID được lấy từ JWT 5. **POST /admin/subscriptions/adjust** - Điều chỉnh gói đăng ký - Body: `AdjustSubscriptionDto` {userId, newPlanTier, reason} - Lệnh: `AdjustSubscriptionCommand(userId, adminId, newPlanTier, reason)` 6. **GET /admin/dashboard** - Thống kê dashboard - Truy vấn: `GetDashboardStatsQuery` 7. **GET /admin/revenue** - Thống kê doanh thu - Tham số truy vấn: startDate, endDate, groupBy (day/month) ### AdminModerationController (Kiểm Duyệt Nội Dung) **File**: `presentation/controllers/admin-moderation.controller.ts` #### Các Endpoint: 1. **GET /admin/moderation** - Lấy hàng đợi kiểm duyệt - Tham số truy vấn: page, limit - Trả về: ModerationQueueResult 2. **POST /admin/moderation/approve** - Phê duyệt listing - Body: `ApproveListingDto` {listingId, moderationNotes} - Lệnh: `ApproveListingCommand(listingId, adminId, moderationNotes)` - Sự kiện: `ListingApprovedEvent` được phát hành 3. **POST /admin/moderation/reject** - Từ chối listing - Body: `RejectListingDto` {listingId, reason} - Lệnh: `RejectListingCommand(listingId, adminId, reason)` - Sự kiện: `ListingRejectedEvent` được phát hành 4. **POST /admin/moderation/bulk** - Kiểm duyệt hàng loạt - Body: `BulkModerateDto` {listingIds[], action, reason} - Lệnh: `BulkModerateListingsCommand(...)` 5. **GET /admin/kyc** - Lấy hàng đợi KYC - Trả về: KycQueueResult (người dùng có KYC PENDING) 6. **POST /admin/kyc/approve** - Phê duyệt KYC - Body: `ApproveKycDto` {userId, comments} - Lệnh: `ApproveKycCommand(userId, adminId, comments)` - Sự kiện: `KycApprovedEvent` được phát hành 7. **POST /admin/kyc/reject** - Từ chối KYC - Body: `RejectKycDto` {userId, reason} - Lệnh: `RejectKycCommand(userId, adminId, reason)` - Sự kiện: `KycRejectedEvent` được phát hành ### Mẫu Chính: Lấy Admin ID ```typescript @Post('moderation/approve') async approveListing( @Body() dto: ApproveListingDto, @CurrentUser() user: JwtPayload, // ← Danh tính Admin ) { return this.commandBus.execute( new ApproveListingCommand(dto.listingId, user.sub, dto.moderationNotes) ); // user.sub = userId của admin } ``` --- ## 4. CƠ SỞ HẠ TẦNG SỰ KIỆN/GHI LOG HIỆN CÓ ### Giao Diện DomainEvent **Vị trí**: `@modules/shared` ```typescript // Tất cả sự kiện domain đều triển khai DomainEvent: export interface DomainEvent { readonly eventName: string; readonly occurredAt: Date; readonly aggregateId: string; // Những gì đã thay đổi (user/listing ID) } ``` ### Ví dụ: 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 đã thực hiện hành động public readonly reason: string, ) {} } ``` ### Ví dụ: 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 đã phê duyệt public readonly moderationNotes?: string, ) {} } ``` ### Phát Hành & Lắng Nghe Sự Kiện **Mẫu Sử Dụng**: NestJS CQRS + EventEmitter #### Phát Hành (trong Command Handlers): ```typescript @CommandHandler(BanUserCommand) export class BanUserHandler implements ICommandHandler { constructor( private readonly userRepo: IUserRepository, private readonly eventBus: EventBus, // ← Được inject ) {} async execute(command: BanUserCommand): Promise { // ... 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' }; } } ``` #### Lắng Nghe (trong 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 { this.logger.log( `Handling user.banned for user ${event.aggregateId}`, 'UserBannedListener' ); // Hiệu ứng phụ: vô hiệu hoá listing, gửi thông báo, v.v. const deactivated = await this.prisma.listing.updateMany({ where: { sellerId: event.aggregateId, status: { in: ['ACTIVE', ...] } }, data: { status: 'EXPIRED' }, }); // Gửi thông báo email await this.commandBus.execute( new SendNotificationCommand(user.id, 'EMAIL', 'user.banned', ...) ); } } ``` ### Kiến Trúc EventBus - **Mô-đun**: `@nestjs/cqrs` - **Thiết lập**: `CqrsModule.forRoot()` trong `app.module.ts` - **Cơ chế**: - Lệnh phát hành sự kiện qua `eventBus.publish(event)` - Listener đăng ký qua `@OnEvent(eventName, { async: true })` - Sự kiện mặc định là bất đồng bộ (không chặn) --- ## 5. DỊCH VỤ LOGGER **Vị trí**: `apps/api/src/modules/shared/infrastructure/logger.service.ts` ### Tính Năng - **Nhà cung cấp**: Pino (ghi log có cấu trúc) - **Che giấu PII**: Tự động ẩn các trường nhạy cảm - Đường dẫn bị che giấu: password, token, email, phone, kycData, creditCard, v.v. - Mẫu kiểm duyệt: `[REDACTED]` - **Nhận biết môi trường**: - Dev: In đẹp có màu sắc - Prod: JSON có cấu trúc - **Các phương thức**: - `log(message, context)` - `error(message, trace, context)` - `warn(message, context)` - `debug(message, context)` - `verbose(message, context)` - `child(bindings)` - Logger con có ràng buộc ngữ cảnh ### Các Trường Bị Che Giấu ```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. XỬ LÝ NGOẠI LỆ & BỘ LỌC ### GlobalExceptionFilter **Vị trí**: `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(); const request = ctx.getRequest(); 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); } } ``` ### Cấu Trúc Phản Hồi Lỗi ```typescript interface ErrorResponseBody { statusCode: number; errorCode: ErrorCode; // Enum với các giá trị như VALIDATION_FAILED, NOT_FOUND, v.v. message: string; details?: Record; correlationId?: string; timestamp: string; } ``` ### Ngoại Lệ Domain ```typescript export class DomainException extends HttpException { constructor( public readonly errorCode: ErrorCode, message: string, statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR, public readonly details?: Record, ) { super(message, statusCode); } } // Các ngoại lệ cụ thể: 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. BẢO MẬT & GUARD ### Kiểm Soát Truy Cập Dựa Trên Vai Trò (RBAC) **Vị trí**: `apps/api/src/modules/auth/presentation/decorators/` #### Mẫu: ```typescript @Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN') export class AdminController { // Tất cả endpoint yêu cầu vai trò ADMIN } ``` #### Luồng Roles Guard: 1. `JwtAuthGuard` - Xác thực token JWT 2. `RolesGuard` - Kiểm tra decorator `@Roles()` với user.role 3. Cả hai decorator từ `@modules/auth` ### Giới Hạn Tốc Độ **Thiết lập**: ThrottlerModule + ThrottlerBehindProxyGuard - Mặc định: 60 yêu cầu mỗi 60 giây mỗi IP - Endpoint xác thực: 10 yêu cầu mỗi 60 giây - Callback thanh toán: 20 yêu cầu mỗi 60 giây --- ## 8. CẤU TRÚC TẦNG DDD ### Các Tầng Kiến Trúc ``` Tầng Trình Bày (Controllers) ↓ (Xác thực DTO) Tầng Ứng Dụng (Commands/Queries/Handlers/Listeners) ↓ (Command/Query) Tầng Domain (Events, Interfaces, Business Rules) ↓ (Gọi Repository, Phát hành Event) Tầng Cơ Sở Hạ Tầng (Prisma, Database) ``` ### Mẫu Command Handler ```typescript // 1. Command (giống DTO) 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 + Phát hành Event) @CommandHandler(BanUserCommand) export class BanUserHandler implements ICommandHandler { constructor( @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, private readonly eventBus: EventBus, ) {} async execute(command: BanUserCommand): Promise { // Business logic const user = await this.userRepo.findById(command.userId); if (!user) throw new NotFoundException(...); user.deactivate(); await this.userRepo.update(user); // Phát hành event this.eventBus.publish( new UserBannedEvent(user.id, command.adminId, command.reason) ); return { userId: user.id, isActive: false, message: '...' }; } } // 3. Event (Kích hoạt Hiệu Ứng Phụ) 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 (Hiệu Ứng Phụ - được kích hoạt bởi Event) @Injectable() export class UserBannedListener { @OnEvent('user.banned', { async: true }) async handle(event: UserBannedEvent): Promise { // Gửi thông báo, cập nhật dữ liệu liên quan, v.v. } } ``` ### Mẫu Query Handler ```typescript // Query (định nghĩa thao tác đọc) export class GetDashboardStatsQuery {} // Handler (lấy & trả về dữ liệu) @QueryHandler(GetDashboardStatsQuery) export class GetDashboardStatsHandler implements IQueryHandler { constructor( @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, ) {} async execute(_query: GetDashboardStatsQuery): Promise { return this.adminQueryRepo.getDashboardStats(); } } ``` ### Mẫu Repository ```typescript // Giao diện Domain (không có chi tiết triển khai) export interface IAdminQueryRepository { getModerationQueue(page: number, limit: number): Promise; getDashboardStats(): Promise; // ... thêm phương thức } // Triển khai cơ sở hạ tầng (dành riêng cho Prisma) @Injectable() export class PrismaAdminQueryRepository implements IAdminQueryRepository { constructor(private readonly prisma: PrismaService) {} async getModerationQueue(page: number, limit: number): Promise { // Truy vấn Prisma ở đây } } // Token DI export const ADMIN_QUERY_REPOSITORY = Symbol('ADMIN_QUERY_REPOSITORY'); // Đăng ký mô-đun @Module({ providers: [ { provide: ADMIN_QUERY_REPOSITORY, useClass: PrismaAdminQueryRepository }, ], }) export class AdminModule {} ``` --- ## 9. BOOTSTRAP MÔ-ĐUN ### Thiết Lập AdminModule **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 handler ...QueryHandlers, // 6 query handler // Event Listeners UserBannedListener, UserDeactivatedListener, ], }) export class AdminModule {} ``` ### Thiết Lập Toàn Cục **File**: `apps/api/src/app.module.ts` ```typescript @Module({ imports: [ SentryModule.forRoot(), CqrsModule.forRoot(), // ← CQRS với Event Bus ScheduleModule.forRoot(), ThrottlerModule.forRoot(...), // ← Giới hạn tốc độ // ... các mô-đun khác bao gồm 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. CÁC MẪU INTERCEPTOR HIỆN CÓ ### HttpMetricsInterceptor **Vị trí**: `@modules/metrics` - Được inject toàn cục qua `APP_INTERCEPTOR` - Theo dõi các số liệu yêu cầu/phản hồi HTTP - Có thể dùng làm mẫu cho interceptor ghi log kiểm toán ### CSRF Middleware **Vị trí**: `@modules/shared/infrastructure/middleware/csrf.middleware` - Mẫu double-submit cookie - Xác thực trên các phương thức thay đổi trạng thái - Cung cấp mô hình để tăng cường ngữ cảnh yêu cầu --- ## 11. TÓM TẮT CHO TRIỂN KHAI GHI LOG KIỂM TOÁN ### Những Gì Đã Có Sẵn ✅ 1. **Kiến trúc hướng sự kiện** - Lệnh phát hành sự kiện, listener xử lý hiệu ứng phụ 2. **Lấy danh tính admin** - Tất cả hành động admin đều có `adminId` trong lệnh 3. **Dịch vụ Logger** - Dựa trên Pino với che giấu PII 4. **Xử lý ngoại lệ** - Bộ lọc toàn cục + phân cấp DomainException 5. **RBAC** - Guard @Roles('ADMIN') đã có sẵn 6. **Bootstrap mô-đun** - Mẫu DI rõ ràng sẵn sàng để inject audit repository 7. **Xác thực DTO** - Tất cả đầu vào được xác thực qua class-validator ### Những Gì Cần Xây Dựng 🚀 1. **Model Prisma AuditLog** - Lưu trữ trong cơ sở dữ liệu 2. **AuditLoggingInterceptor** - Lấy ngữ cảnh HTTP (IP, timestamp, endpoint) 3. **Domain Event AuditEvent** - Mở rộng sự kiện domain cho mục đích kiểm toán 4. **AuditLoggingListener** - Listener sự kiện lưu vào AuditLog 5. **AuditLog Repository** - Thao tác CRUD cho AuditLog 6. **Query Handler** - Truy xuất log kiểm toán với bộ lọc (khoảng thời gian, admin, loại hành động) 7. **Endpoint Controller** - GET /admin/audit-logs để xem nhật ký kiểm toán ### Các Điểm Tích Hợp Chính 1. Lệnh đã truyền `adminId` → Sử dụng trong AuditLoggingListener 2. Sự kiện domain đã được phát hành → Móc listener kiểm toán vào các sự kiện liên quan 3. HTTPContext (IP, user-agent, v.v.) → Lấy trong interceptor 4. Dịch vụ Logger có sẵn → Sử dụng để ghi log có cấu trúc 5. Mẫu Repository đã được thiết lập → Tuân theo cho AuditLog repository ### Các Trường Kiểm Toán Được Đề Xuất - `id` (khoá chính) - `adminId` (người đã thực hiện hành động) - `adminName` (để phi chuẩn hoá) - `action` (ví dụ: 'user.banned', 'listing.approved') - `resourceType` (ví dụ: 'user', 'listing') - `resourceId` (những gì bị ảnh hưởng) - `changes` (JSON những gì đã thay đổi - trước/sau) - `reason` (từ DTO nếu được cung cấp) - `ipAddress` (từ yêu cầu) - `userAgent` (từ yêu cầu) - `status` (thành công/thất bại) - `statusCode` (HTTP status) - `errorMessage` (nếu thất bại) - `duration` (mili giây) - `createdAt` (dấu thời gian) --- ## Tham Chiếu Các File Chính ### Controllers - `presentation/controllers/admin.controller.ts` - Các thao tác admin chính - `presentation/controllers/admin-moderation.controller.ts` - Kiểm duyệt & KYC ### Command Handlers (Điểm Hành Động) - `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 (Nơi Móc Kiểm Toán) - `application/listeners/user-banned.listener.ts` - `application/listeners/user-deactivated.listener.ts` ### Cơ Sở Hạ Tầng - `infrastructure/repositories/prisma-admin-query.repository.ts` - `infrastructure/repositories/admin-stats.queries.ts` - `infrastructure/repositories/admin-user.queries.ts` ### Tài Nguyên Dùng Chung - 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 dòng, chưa có model kiểm toán) - Đảm bảo type safety của Prisma Client --- ## Các Bước Tiếp Theo 1. ✅ **Thiết Kế Schema** - Tạo model AuditLog trong Prisma 2. ✅ **Mẫu Repository** - Tạo AuditLogRepository với giao diện 3. ✅ **Sự Kiện Kiểm Toán** - Tạo sự kiện domain cho mục đích kiểm toán 4. ✅ **Event Listener** - Tạo AuditLoggingListener để lưu trữ sự kiện 5. ✅ **Interceptor** - Lấy ngữ cảnh HTTP (tuỳ chọn nâng cao) 6. ✅ **Query Handler** - Tạo query/handler để truy xuất log kiểm toán 7. ✅ **Endpoint Controller** - Thêm endpoint GET /admin/audit-logs 8. ✅ **Kiểm Thử** - Kiểm thử đơn vị và tích hợp 9. ✅ **Tài Liệu** - Tài liệu API trong Swagger