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,2 @@
|
||||
export { JwtStrategy } from './jwt.strategy';
|
||||
export { LocalStrategy } from './local.strategy';
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { type JwtPayload } from '../services/token.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: process.env['JWT_SECRET'] || 'goodgo-jwt-secret-change-in-production',
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: JwtPayload): JwtPayload {
|
||||
return { sub: payload.sub, phone: payload.phone, role: payload.role };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY)
|
||||
private readonly userRepo: IUserRepository,
|
||||
) {
|
||||
super({ usernameField: 'phone', passwordField: 'password' });
|
||||
}
|
||||
|
||||
async validate(phone: string, password: string): Promise<{ id: string; phone: string; role: string }> {
|
||||
const normalizedPhone = normalizeVietnamPhone(phone);
|
||||
if (!normalizedPhone) {
|
||||
throw new UnauthorizedException('Số điện thoại không hợp lệ');
|
||||
}
|
||||
const user = await this.userRepo.findByPhone(normalizedPhone);
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
throw new UnauthorizedException('Số điện thoại hoặc mật khẩu không đúng');
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
throw new UnauthorizedException('Tài khoản đã bị vô hiệu hóa');
|
||||
}
|
||||
|
||||
const isValid = await user.passwordHash.compare(password);
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('Số điện thoại hoặc mật khẩu không đúng');
|
||||
}
|
||||
|
||||
return { id: user.id, phone: user.phone.value, role: user.role };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user