26 KiB
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)
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)
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:
-
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)
-
GET /admin/users/:id - Lấy chi tiết người dùng
- Trả về: UserDetail (thông tin đầy đủ + hoạt động)
-
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)
- Body:
-
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
- Body:
-
POST /admin/subscriptions/adjust - Điều chỉnh gói đăng ký
- Body:
AdjustSubscriptionDto{userId, newPlanTier, reason} - Lệnh:
AdjustSubscriptionCommand(userId, adminId, newPlanTier, reason)
- Body:
-
GET /admin/dashboard - Thống kê dashboard
- Truy vấn:
GetDashboardStatsQuery
- Truy vấn:
-
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:
-
GET /admin/moderation - Lấy hàng đợi kiểm duyệt
- Tham số truy vấn: page, limit
- Trả về: ModerationQueueResult
-
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
- Body:
-
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
- Body:
-
POST /admin/moderation/bulk - Kiểm duyệt hàng loạt
- Body:
BulkModerateDto{listingIds[], action, reason} - Lệnh:
BulkModerateListingsCommand(...)
- Body:
-
GET /admin/kyc - Lấy hàng đợi KYC
- Trả về: KycQueueResult (người dùng có KYC PENDING)
-
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
- Body:
-
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
- Body:
Mẫu Chính: Lấy Admin ID
@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
// 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
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
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):
@CommandHandler(BanUserCommand)
export class BanUserHandler implements ICommandHandler<BanUserCommand> {
constructor(
private readonly userRepo: IUserRepository,
private readonly eventBus: EventBus, // ← Được inject
) {}
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' };
}
}
Lắng Nghe (trong Event Listeners):
@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'
);
// 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()trongapp.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)
- Lệnh phát hành sự kiện qua
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
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
@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);
}
}
Cấu Trúc Phản Hồi Lỗi
interface ErrorResponseBody {
statusCode: number;
errorCode: ErrorCode; // Enum với các giá trị như VALIDATION_FAILED, NOT_FOUND, v.v.
message: string;
details?: Record<string, unknown>;
correlationId?: string;
timestamp: string;
}
Ngoại Lệ Domain
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);
}
}
// 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:
@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:
JwtAuthGuard- Xác thực token JWTRolesGuard- Kiểm tra decorator@Roles()với user.role- 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
// 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<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);
// 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<void> {
// Gửi thông báo, cập nhật dữ liệu liên quan, v.v.
}
}
Mẫu Query Handler
// 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<GetDashboardStatsQuery> {
constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
) {}
async execute(_query: GetDashboardStatsQuery): Promise<DashboardStats> {
return this.adminQueryRepo.getDashboardStats();
}
}
Mẫu Repository
// Giao diện Domain (không có chi tiết triển khai)
export interface IAdminQueryRepository {
getModerationQueue(page: number, limit: number): Promise<ModerationQueueResult>;
getDashboardStats(): Promise<DashboardStats>;
// ... 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<ModerationQueueResult> {
// 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
@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
@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 ✅
- 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ụ
- Lấy danh tính admin - Tất cả hành động admin đều có
adminIdtrong lệnh - Dịch vụ Logger - Dựa trên Pino với che giấu PII
- Xử lý ngoại lệ - Bộ lọc toàn cục + phân cấp DomainException
- RBAC - Guard @Roles('ADMIN') đã có sẵn
- Bootstrap mô-đun - Mẫu DI rõ ràng sẵn sàng để inject audit repository
- 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 🚀
- Model Prisma AuditLog - Lưu trữ trong cơ sở dữ liệu
- AuditLoggingInterceptor - Lấy ngữ cảnh HTTP (IP, timestamp, endpoint)
- Domain Event AuditEvent - Mở rộng sự kiện domain cho mục đích kiểm toán
- AuditLoggingListener - Listener sự kiện lưu vào AuditLog
- AuditLog Repository - Thao tác CRUD cho AuditLog
- 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)
- Endpoint Controller - GET /admin/audit-logs để xem nhật ký kiểm toán
Các Điểm Tích Hợp Chính
- Lệnh đã truyền
adminId→ Sử dụng trong AuditLoggingListener - 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
- HTTPContext (IP, user-agent, v.v.) → Lấy trong interceptor
- Dịch vụ Logger có sẵn → Sử dụng để ghi log có cấu trúc
- 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ínhpresentation/controllers/admin-moderation.controller.ts- Kiểm duyệt & KYC
Command Handlers (Điểm Hành Động)
application/commands/ban-user/ban-user.handler.tsapplication/commands/approve-listing/approve-listing.handler.tsapplication/commands/approve-kyc/approve-kyc.handler.tsapplication/commands/reject-listing/reject-listing.handler.tsapplication/commands/reject-kyc/reject-kyc.handler.tsapplication/commands/adjust-subscription/adjust-subscription.handler.tsapplication/commands/update-user-status/update-user-status.handler.tsapplication/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts
Event Listeners (Nơi Móc Kiểm Toán)
application/listeners/user-banned.listener.tsapplication/listeners/user-deactivated.listener.ts
Cơ Sở Hạ Tầng
infrastructure/repositories/prisma-admin-query.repository.tsinfrastructure/repositories/admin-stats.queries.tsinfrastructure/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
- ✅ Thiết Kế Schema - Tạo model AuditLog trong Prisma
- ✅ Mẫu Repository - Tạo AuditLogRepository với giao diện
- ✅ Sự Kiện Kiểm Toán - Tạo sự kiện domain cho mục đích kiểm toán
- ✅ Event Listener - Tạo AuditLoggingListener để lưu trữ sự kiện
- ✅ Interceptor - Lấy ngữ cảnh HTTP (tuỳ chọn nâng cao)
- ✅ Query Handler - Tạo query/handler để truy xuất log kiểm toán
- ✅ Endpoint Controller - Thêm endpoint GET /admin/audit-logs
- ✅ Kiểm Thử - Kiểm thử đơn vị và tích hợp
- ✅ Tài Liệu - Tài liệu API trong Swagger