feat(auth): implement Auth module with register, login, JWT, guards, and CQRS
- Add RefreshToken and OAuthAccount models to Prisma schema - Implement clean architecture: domain (entities, VOs, events, repo interfaces), infrastructure (Prisma repos, Passport strategies, token service), application (CQRS command/query handlers), presentation (controller, guards, DTOs) - Endpoints: POST /auth/register, /auth/login, /auth/refresh, GET /auth/profile, GET /auth/profile/agent, PATCH /auth/kyc - JWT access + refresh token rotation with family-based revocation - Role-based guards (BUYER, SELLER, AGENT, ADMIN) - 16 unit tests (value objects, entity) + integration test suite - All 80 tests passing, clean TypeScript build Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
|
||||
import { GetAgentByUserIdQuery } from './get-agent-by-user-id.query';
|
||||
|
||||
export interface AgentDto {
|
||||
id: string;
|
||||
userId: string;
|
||||
licenseNumber: string | null;
|
||||
agency: string | null;
|
||||
qualityScore: number;
|
||||
totalDeals: number;
|
||||
responseTimeAvg: number | null;
|
||||
bio: string | null;
|
||||
serviceAreas: unknown;
|
||||
isVerified: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@QueryHandler(GetAgentByUserIdQuery)
|
||||
export class GetAgentByUserIdHandler implements IQueryHandler<GetAgentByUserIdQuery> {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async execute(query: GetAgentByUserIdQuery): Promise<AgentDto | null> {
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: query.userId },
|
||||
});
|
||||
|
||||
if (!agent) return null;
|
||||
|
||||
return {
|
||||
id: agent.id,
|
||||
userId: agent.userId,
|
||||
licenseNumber: agent.licenseNumber,
|
||||
agency: agent.agency,
|
||||
qualityScore: agent.qualityScore,
|
||||
totalDeals: agent.totalDeals,
|
||||
responseTimeAvg: agent.responseTimeAvg,
|
||||
bio: agent.bio,
|
||||
serviceAreas: agent.serviceAreas,
|
||||
isVerified: agent.isVerified,
|
||||
createdAt: agent.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class GetAgentByUserIdQuery {
|
||||
constructor(public readonly userId: string) {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { Inject, NotFoundException } from '@nestjs/common';
|
||||
import { GetProfileQuery } from './get-profile.query';
|
||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
||||
|
||||
export interface UserProfileDto {
|
||||
id: string;
|
||||
email: string | null;
|
||||
phone: string;
|
||||
fullName: string;
|
||||
avatarUrl: string | null;
|
||||
role: string;
|
||||
kycStatus: string;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@QueryHandler(GetProfileQuery)
|
||||
export class GetProfileHandler implements IQueryHandler<GetProfileQuery> {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
) {}
|
||||
|
||||
async execute(query: GetProfileQuery): Promise<UserProfileDto> {
|
||||
const user = await this.userRepo.findById(query.userId);
|
||||
if (!user) {
|
||||
throw new NotFoundException('Người dùng không tồn tại');
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email?.value ?? null,
|
||||
phone: user.phone.value,
|
||||
fullName: user.fullName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
role: user.role,
|
||||
kycStatus: user.kycStatus,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class GetProfileQuery {
|
||||
constructor(public readonly userId: string) {}
|
||||
}
|
||||
Reference in New Issue
Block a user