fix: production readiness — resolve build, lint, and code quality issues

- Fix Next.js build failure: remove duplicate route at (dashboard)/listings/[id]
  that conflicted with (public)/listings/[id] (same URL path in two route groups)
- Fix 772 ESLint errors: auto-fix import ordering (import-x/order), remove unused
  imports/variables, convert empty interfaces to type aliases, replace require()
  with ESM imports, fix consistent-type-imports violations
- Add CLAUDE.md for developer onboarding documentation
- All checks pass: 0 lint errors, typecheck clean, 230 tests passing, build success

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 07:15:06 +07:00
parent afa70320f5
commit 2502aa69b7
239 changed files with 746 additions and 984 deletions

View File

@@ -1,11 +1,11 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { Test, type TestingModule } from '@nestjs/testing';
import { type INestApplication, ValidationPipe } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { Test, type TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { AuthModule } from '../auth.module';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
import { SharedModule } from '@modules/shared/shared.module';
import { AuthModule } from '../auth.module';
describe('Auth Controller (Integration)', () => {
let app: INestApplication;

View File

@@ -1,6 +1,6 @@
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { LoginUserCommand } from './login-user.command';
import { TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
@CommandHandler(LoginUserCommand)
export class LoginUserHandler implements ICommandHandler<LoginUserCommand> {

View File

@@ -1,9 +1,9 @@
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { UnauthorizedException } from '@modules/shared/domain/domain-exception';
import { RefreshTokenCommand } from './refresh-token.command';
import { TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { RefreshTokenCommand } from './refresh-token.command';
@CommandHandler(RefreshTokenCommand)
export class RefreshTokenHandler implements ICommandHandler<RefreshTokenCommand> {

View File

@@ -1,14 +1,14 @@
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { ConflictException, ValidationException } from '@modules/shared/domain/domain-exception';
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2';
import { RegisterUserCommand } from './register-user.command';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { ConflictException, ValidationException } from '@modules/shared/domain/domain-exception';
import { UserEntity } from '../../../domain/entities/user.entity';
import { Phone } from '../../../domain/value-objects/phone.vo';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { Email } from '../../../domain/value-objects/email.vo';
import { HashedPassword } from '../../../domain/value-objects/hashed-password.vo';
import { TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { Phone } from '../../../domain/value-objects/phone.vo';
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { RegisterUserCommand } from './register-user.command';
@CommandHandler(RegisterUserCommand)
export class RegisterUserHandler implements ICommandHandler<RegisterUserCommand> {

View File

@@ -1,9 +1,9 @@
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { NotFoundException } from '@modules/shared/domain/domain-exception';
import { CacheService, CachePrefix } from '@modules/shared/infrastructure/cache.service';
import { VerifyKycCommand } from './verify-kyc.command';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { VerifyKycCommand } from './verify-kyc.command';
@CommandHandler(VerifyKycCommand)
export class VerifyKycHandler implements ICommandHandler<VerifyKycCommand> {

View File

@@ -1,6 +1,6 @@
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { type PrismaService } from '@modules/shared/infrastructure/prisma.service';
import { GetAgentByUserIdQuery } from './get-agent-by-user-id.query';
export interface AgentDto {

View File

@@ -1,9 +1,9 @@
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { NotFoundException } from '@modules/shared/domain/domain-exception';
import { CacheService, CachePrefix, CacheTTL } from '@modules/shared/infrastructure/cache.service';
import { GetProfileQuery } from './get-profile.query';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { GetProfileQuery } from './get-profile.query';
export interface UserProfileDto {
id: string;

View File

@@ -2,27 +2,19 @@ import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
// Domain
import { USER_REPOSITORY } from './domain/repositories/user.repository';
import { REFRESH_TOKEN_REPOSITORY } from './domain/repositories/refresh-token.repository';
// Infrastructure
import { PrismaUserRepository } from './infrastructure/repositories/prisma-user.repository';
import { PrismaRefreshTokenRepository } from './infrastructure/repositories/prisma-refresh-token.repository';
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
import { LocalStrategy } from './infrastructure/strategies/local.strategy';
import { TokenService } from './infrastructure/services/token.service';
// Application
import { RegisterUserHandler } from './application/commands/register-user/register-user.handler';
import { LoginUserHandler } from './application/commands/login-user/login-user.handler';
import { RefreshTokenHandler } from './application/commands/refresh-token/refresh-token.handler';
import { RegisterUserHandler } from './application/commands/register-user/register-user.handler';
import { VerifyKycHandler } from './application/commands/verify-kyc/verify-kyc.handler';
import { GetProfileHandler } from './application/queries/get-profile/get-profile.handler';
import { GetAgentByUserIdHandler } from './application/queries/get-agent-by-user-id/get-agent-by-user-id.handler';
// Presentation
import { GetProfileHandler } from './application/queries/get-profile/get-profile.handler';
import { REFRESH_TOKEN_REPOSITORY } from './domain/repositories/refresh-token.repository';
import { USER_REPOSITORY } from './domain/repositories/user.repository';
import { PrismaRefreshTokenRepository } from './infrastructure/repositories/prisma-refresh-token.repository';
import { PrismaUserRepository } from './infrastructure/repositories/prisma-user.repository';
import { TokenService } from './infrastructure/services/token.service';
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
import { LocalStrategy } from './infrastructure/strategies/local.strategy';
import { AuthController } from './presentation/controllers/auth.controller';
const CommandHandlers = [

View File

@@ -1,9 +1,9 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { UserEntity } from '../entities/user.entity';
import { Phone } from '../value-objects/phone.vo';
import { HashedPassword } from '../value-objects/hashed-password.vo';
import { Email } from '../value-objects/email.vo';
import { UserRegisteredEvent } from '../events/user-registered.event';
import { Email } from '../value-objects/email.vo';
import { HashedPassword } from '../value-objects/hashed-password.vo';
import { Phone } from '../value-objects/phone.vo';
describe('UserEntity', () => {
let phone: Phone;

View File

@@ -1,9 +1,9 @@
import { AggregateRoot } from '@modules/shared/domain/aggregate-root';
import { type UserRole, type KYCStatus } from '@prisma/client';
import { AggregateRoot } from '@modules/shared/domain/aggregate-root';
import { UserRegisteredEvent } from '../events/user-registered.event';
import { type Email } from '../value-objects/email.vo';
import { type Phone } from '../value-objects/phone.vo';
import { type HashedPassword } from '../value-objects/hashed-password.vo';
import { type Phone } from '../value-objects/phone.vo';
export interface UserProps {
email: Email | null;

View File

@@ -1,5 +1,5 @@
import { type DomainEvent } from '@modules/shared/domain/domain-event';
import { type UserRole } from '@prisma/client';
import { type DomainEvent } from '@modules/shared/domain/domain-event';
export class UserRegisteredEvent implements DomainEvent {
readonly eventName = 'user.registered';

View File

@@ -1,5 +1,5 @@
import { ValueObject } from '@modules/shared/domain/value-object';
import { Result } from '@modules/shared/domain/result';
import { ValueObject } from '@modules/shared/domain/value-object';
interface EmailProps {
value: string;

View File

@@ -1,6 +1,6 @@
import { ValueObject } from '@modules/shared/domain/value-object';
import { Result } from '@modules/shared/domain/result';
import * as bcrypt from 'bcrypt';
import { Result } from '@modules/shared/domain/result';
import { ValueObject } from '@modules/shared/domain/value-object';
interface HashedPasswordProps {
value: string;

View File

@@ -1,5 +1,5 @@
import { ValueObject } from '@modules/shared/domain/value-object';
import { Result } from '@modules/shared/domain/result';
import { ValueObject } from '@modules/shared/domain/value-object';
import { isValidVietnamPhone, normalizeVietnamPhone } from '@modules/shared/utils/vietnam-phone.validator';
interface PhoneProps {

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
import { type PrismaService } from '@modules/shared/infrastructure/prisma.service';
import {
type IRefreshTokenRepository,
type RefreshTokenRecord,

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
import { type User as PrismaUser } from '@prisma/client';
import { type IUserRepository } from '../../domain/repositories/user.repository';
import { type PrismaService } from '@modules/shared/infrastructure/prisma.service';
import { UserEntity, type UserProps } from '../../domain/entities/user.entity';
import { type IUserRepository } from '../../domain/repositories/user.repository';
import { Email } from '../../domain/value-objects/email.vo';
import { Phone } from '../../domain/value-objects/phone.vo';
import { HashedPassword } from '../../domain/value-objects/hashed-password.vo';
import { Phone } from '../../domain/value-objects/phone.vo';
@Injectable()
export class PrismaUserRepository implements IUserRepository {

View File

@@ -1,9 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { randomBytes, createHash } from 'crypto';
import { Inject, Injectable } from '@nestjs/common';
import { type JwtService } from '@nestjs/jwt';
import {
REFRESH_TOKEN_REPOSITORY,
IRefreshTokenRepository,
type IRefreshTokenRepository,
} from '../../domain/repositories/refresh-token.repository';
export interface JwtPayload {

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import type { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { type JwtPayload } from '../services/token.service';
function extractJwtFromCookieOrHeader(req: Request): string | null {

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { USER_REPOSITORY, type IUserRepository } from '../../domain/repositories/user.repository';
import { normalizeVietnamPhone } from '@modules/shared/utils/vietnam-phone.validator';
import { USER_REPOSITORY, type IUserRepository } from '../../domain/repositories/user.repository';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {

View File

@@ -9,28 +9,28 @@ import {
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import type { Request, Response } from 'express';
import { RegisterUserCommand } from '../../application/commands/register-user/register-user.command';
import { LoginUserCommand } from '../../application/commands/login-user/login-user.command';
import { RefreshTokenCommand } from '../../application/commands/refresh-token/refresh-token.command';
import { RegisterUserCommand } from '../../application/commands/register-user/register-user.command';
import { VerifyKycCommand } from '../../application/commands/verify-kyc/verify-kyc.command';
import { GetProfileQuery } from '../../application/queries/get-profile/get-profile.query';
import { type AgentDto } from '../../application/queries/get-agent-by-user-id/get-agent-by-user-id.handler';
import { GetAgentByUserIdQuery } from '../../application/queries/get-agent-by-user-id/get-agent-by-user-id.query';
import { RegisterDto } from '../dto/register.dto';
import { type UserProfileDto } from '../../application/queries/get-profile/get-profile.handler';
import { GetProfileQuery } from '../../application/queries/get-profile/get-profile.query';
import { type TokenService, type JwtPayload, type TokenPair } from '../../infrastructure/services/token.service';
import { CurrentUser } from '../decorators/current-user.decorator';
import { Roles } from '../decorators/roles.decorator';
import { LoginDto } from '../dto/login.dto';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
import { VerifyKycDto } from '../dto/verify-kyc.dto';
import { type RefreshTokenDto } from '../dto/refresh-token.dto';
import { type RegisterDto } from '../dto/register.dto';
import { type VerifyKycDto } from '../dto/verify-kyc.dto';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { LocalAuthGuard } from '../guards/local-auth.guard';
import { RolesGuard } from '../guards/roles.guard';
import { CurrentUser } from '../decorators/current-user.decorator';
import { Roles } from '../decorators/roles.decorator';
import { TokenService, type JwtPayload, type TokenPair } from '../../infrastructure/services/token.service';
import { type UserProfileDto } from '../../application/queries/get-profile/get-profile.handler';
import { type AgentDto } from '../../application/queries/get-agent-by-user-id/get-agent-by-user-id.handler';
const IS_PRODUCTION = process.env['NODE_ENV'] === 'production';
const ACCESS_TOKEN_MAX_AGE = 15 * 60 * 1000; // 15 minutes

View File

@@ -1,5 +1,5 @@
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class LoginDto {
@ApiProperty({ example: '0901234567' })

View File

@@ -1,5 +1,5 @@
import { IsOptional, IsString } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class RefreshTokenDto {
@ApiPropertyOptional({ description: 'JWT refresh token (optional if sent via cookie)' })

View File

@@ -1,5 +1,5 @@
import { IsString, IsOptional, IsEmail, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsEmail, MinLength } from 'class-validator';
export class RegisterDto {
@ApiProperty({ example: '0901234567', description: 'Phone number' })

View File

@@ -1,6 +1,6 @@
import { IsEnum, IsOptional, IsObject } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { KYCStatus } from '@prisma/client';
import { IsEnum, IsOptional, IsObject } from 'class-validator';
export class VerifyKycDto {
@ApiProperty({ enum: KYCStatus, description: 'New KYC status' })

View File

@@ -1,5 +1,5 @@
import { Injectable, Logger, type CanActivate, type ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { type Reflector } from '@nestjs/core';
import { type UserRole } from '@prisma/client';
import { ROLES_KEY } from '../decorators/roles.decorator';