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:
Ho Ngoc Hai
2026-04-08 00:24:42 +07:00
parent c981bff771
commit 391c040100
63 changed files with 2194 additions and 33 deletions

View File

@@ -0,0 +1 @@
export { UserEntity, type UserProps } from './user.entity';

View File

@@ -0,0 +1,88 @@
import { AggregateRoot } from '@modules/shared/domain/aggregate-root';
import { type UserRole, type KYCStatus } from '@prisma/client';
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';
export interface UserProps {
email: Email | null;
phone: Phone;
passwordHash: HashedPassword | null;
fullName: string;
avatarUrl: string | null;
role: UserRole;
kycStatus: KYCStatus;
kycData: unknown;
isActive: boolean;
}
export class UserEntity extends AggregateRoot<string> {
private _email: Email | null;
private _phone: Phone;
private _passwordHash: HashedPassword | null;
private _fullName: string;
private _avatarUrl: string | null;
private _role: UserRole;
private _kycStatus: KYCStatus;
private _kycData: unknown;
private _isActive: boolean;
constructor(id: string, props: UserProps, createdAt?: Date, updatedAt?: Date) {
super(id, createdAt, updatedAt);
this._email = props.email;
this._phone = props.phone;
this._passwordHash = props.passwordHash;
this._fullName = props.fullName;
this._avatarUrl = props.avatarUrl;
this._role = props.role;
this._kycStatus = props.kycStatus;
this._kycData = props.kycData;
this._isActive = props.isActive;
}
get email(): Email | null { return this._email; }
get phone(): Phone { return this._phone; }
get passwordHash(): HashedPassword | null { return this._passwordHash; }
get fullName(): string { return this._fullName; }
get avatarUrl(): string | null { return this._avatarUrl; }
get role(): UserRole { return this._role; }
get kycStatus(): KYCStatus { return this._kycStatus; }
get kycData(): unknown { return this._kycData; }
get isActive(): boolean { return this._isActive; }
static createNew(
id: string,
phone: Phone,
fullName: string,
passwordHash: HashedPassword,
email?: Email,
role: UserRole = 'BUYER',
): UserEntity {
const user = new UserEntity(id, {
email: email ?? null,
phone,
passwordHash,
fullName,
avatarUrl: null,
role,
kycStatus: 'NONE',
kycData: null,
isActive: true,
});
user.addDomainEvent(new UserRegisteredEvent(id, phone.value, role));
return user;
}
updateKycStatus(status: KYCStatus, kycData?: unknown): void {
this._kycStatus = status;
if (kycData !== undefined) this._kycData = kycData;
this.updatedAt = new Date();
}
deactivate(): void {
this._isActive = false;
this.updatedAt = new Date();
}
}