# GoodGo Platform - Complete Authentication & Seed Data Guide **Last Updated:** April 12, 2026 --- ## 1. PASSWORD HASHING ### Implementation - **File:** `apps/api/src/modules/auth/domain/value-objects/hashed-password.vo.ts` - **Algorithm:** bcrypt - **Salt Rounds:** Configurable via `BCRYPT_ROUNDS` env var (default: `12`) - **Min Password Length:** 8 characters ### Key Code ```typescript static readonly SALT_ROUNDS = parseInt( process.env['BCRYPT_ROUNDS'] ?? '12', 10, ); static readonly MIN_LENGTH = 8; static async fromPlain(password: string): Promise> { if (password.length < this.MIN_LENGTH) { return Result.err(`Mật khẩu phải có ít nhất ${this.MIN_LENGTH} ký tự`); } const hash = await bcrypt.hash(password, this.SALT_ROUNDS); return Result.ok(new HashedPassword({ value: hash })); } async compare(plainPassword: string): Promise { return bcrypt.compare(plainPassword, this.props.value); } ``` --- ## 2. PHONE VALIDATION & NORMALIZATION ### Vietnamese Phone Format - **File:** `apps/api/src/modules/shared/utils/vietnam-phone.validator.ts` - **Regex Pattern:** `/^(?:\+84|84|0)(3[2-9]|5[2689]|7[06-9]|8[1-9]|9[0-9])\d{7}$/` ### Valid Patterns - Starts with `+84` (international format) - Starts with `84` (country code without +) - Starts with `0` (local format) ### Carrier Codes (after leading digit) - **3[2-9]:** Mobile (Viettel, VinaPhone, MobiFone) - **5[2689]:** Mobile (Viettel) - **7[06-9]:** Mobile (newer carriers) - **8[1-9]:** Mobile (VinaPhone) - **9[0-9]:** Mobile (MobiFone) ### Normalization Function ```typescript function normalizeVietnamPhone(phone: string): string | null { const cleaned = phone.replace(/[\s.-]/g, ''); // Remove spaces, dots, dashes if (!VN_PHONE_REGEX.test(cleaned)) return null; if (cleaned.startsWith('+84')) return cleaned; if (cleaned.startsWith('84')) return `+${cleaned}`; if (cleaned.startsWith('0')) return `+84${cleaned.slice(1)}`; return null; } ``` ### Examples ``` Input: "0900000001" → Normalized: "+84900000001" Input: "84900000001" → Normalized: "+84900000001" Input: "+84900000001" → Normalized: "+84900000001" ``` --- ## 3. EMAIL VALIDATION & NORMALIZATION ### Implementation - **File:** `apps/api/src/modules/auth/domain/value-objects/email.vo.ts` - **Regex:** `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` - **Normalization:** Trimmed and converted to lowercase ### Code ```typescript static create(email: string): Result { const normalized = email.trim().toLowerCase(); if (!this.EMAIL_REGEX.test(normalized)) { return Result.err('Email không hợp lệ'); } return Result.ok(new Email({ value: normalized })); } ``` --- ## 4. PII ENCRYPTION & HASHING ### Field Encryption Configuration - **File:** `apps/api/src/modules/shared/infrastructure/field-encryption.ts` - **Algorithm:** AES-256-GCM - **Stored Format:** `enc:v{version}:{iv}:{authTag}:{ciphertext}` (hex-encoded) - **Key Size:** 32 bytes (64 hex characters) - **IV Length:** 12 bytes (96-bit) - **Auth Tag Length:** 16 bytes ### Encrypted Fields (User) - `email` → encrypted, with hash in `emailHash` (HMAC-SHA256) - `phone` → encrypted, with hash in `phoneHash` (HMAC-SHA256) - `kycData` → encrypted (no separate hash) ### Hash Computation ```typescript function deriveHmacKey(encryptionKeyHex: string): Buffer { return crypto.hkdfSync( 'sha256', Buffer.from(encryptionKeyHex, 'hex'), Buffer.alloc(0), Buffer.from('goodgo-field-hash', 'utf8'), 32, ); } function computeHash(value: string, hmacKey: Buffer): string { const normalized = value.toLowerCase().trim(); return crypto.createHmac('sha256', hmacKey).update(normalized).digest('hex'); } ``` ### Environment Variables - `FIELD_ENCRYPTION_KEY`: Hex-encoded 32-byte key (required) - `FIELD_ENCRYPTION_KEY_VERSION`: Key version for rotation (default: 1) - Fallback: `KYC_ENCRYPTION_KEY` / `KYC_ENCRYPTION_KEY_VERSION` --- ## 5. LOGIN FLOW ### Local Strategy (Username/Password) - **File:** `apps/api/src/modules/auth/infrastructure/strategies/local.strategy.ts` - **Username Field:** `phone` (Vietnamese phone, normalized) - **Password Field:** `password` (plaintext during login) ### Login Steps 1. **Validate phone format** - normalize Vietnamese phone 2. **Find user by phoneHash** - lookup in `phoneHash` unique index 3. **Check user status** - `user.isActive` must be true 4. **Compare password** - bcrypt.compare(plainPassword, passwordHash) 5. **Check MFA** - if `user.totpEnabled` is true, return MFA challenge 6. **Issue tokens** - if no MFA, generate JWT pair ### Login Response (No MFA) ```json { "requiresMfa": false, "tokens": { "accessToken": "eyJhbGc...", "refreshToken": "eyJhbGc...", "expiresIn": 3600 } } ``` ### Login Response (MFA Required) ```json { "requiresMfa": true, "challengeId": "challenge-id-here" } ``` --- ## 6. USER ROLES ### UserRole Enum ```prisma enum UserRole { BUYER // Default role for new users SELLER // Can list properties AGENT // Professional real estate agent (has Agent profile) ADMIN // Platform administrator } ``` ### Role Details - **BUYER:** Can search, inquire, make offers - **SELLER:** Can create listings, manage properties - **AGENT:** Professional agent with verified profile, license, service areas - **ADMIN:** Full platform access, audit logs, user management --- ## 7. CREATING AN ADMIN USER THAT CAN LOG IN ### ⚠️ Critical Requirements 1. **Valid phone** - Must pass Vietnamese phone validation 2. **Valid email** - Must pass email regex 3. **Hashed password** - Must use bcrypt with ≥12 rounds 4. **Active status** - `isActive: true` 5. **Normalized phone** - Stored in `+84...` format 6. **Hash fields** - Must populate `phoneHash` and `emailHash` for queries ### Node.js/TypeScript Script ```typescript import * as bcrypt from 'bcrypt'; import crypto from 'node:crypto'; import { PrismaClient } from '@prisma/client'; async function createAdminUser() { const prisma = new PrismaClient(); // 1. Hash password with bcrypt const plainPassword = 'AdminPassword123'; const passwordHash = await bcrypt.hash(plainPassword, 12); // 2. Normalize phone const phone = '0900000001'; const normalizedPhone = `+84${phone.slice(1)}`; // '+84900000001' // 3. Compute phone hash const encryptionKey = process.env['FIELD_ENCRYPTION_KEY']; const hmacKey = crypto.hkdfSync( 'sha256', Buffer.from(encryptionKey, 'hex'), Buffer.alloc(0), Buffer.from('goodgo-field-hash', 'utf8'), 32, ); const phoneHash = crypto .createHmac('sha256', hmacKey) .update(normalizedPhone.toLowerCase()) .digest('hex'); // 4. Compute email hash const email = 'admin@goodgo.vn'; const emailHash = crypto .createHmac('sha256', hmacKey) .update(email.toLowerCase()) .digest('hex'); // 5. Create user const admin = await prisma.user.create({ data: { id: 'seed-admin-01', phone: normalizedPhone, phoneHash, email, emailHash, passwordHash, fullName: 'Admin GoodGo', role: 'ADMIN', kycStatus: 'VERIFIED', isActive: true, totpEnabled: false, totpBackupCodes: [], }, }); console.log('Admin user created:', admin.id); await prisma.$disconnect(); } createAdminUser().catch(console.error); ``` ### Login Test ```bash curl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{ "phone": "0900000001", "password": "AdminPassword123" }' ``` --- ## 8. MFA (Multi-Factor Authentication) ### TOTP Setup - **Generator:** otplib (RFC 6238 compliant) - **Period:** 30 seconds - **Digits:** 6-digit codes - **Clock Skew:** ±30 seconds tolerance ### Backup Codes - **Count:** 10 codes - **Length:** 8 characters - **Charset:** A-Z (no O, I), 2-9 (no 0, 1) - **Hashing:** HMAC-SHA256 (not bcrypt) ### For Seed Data - Set `totpEnabled: false` for simplicity - Set `totpSecret: null` - Set `totpBackupCodes: []` --- ## 9. SEED USER EXAMPLE ### Current Seed (from prisma/seed.ts) - NO LOGIN ```typescript const admin = await prisma.user.upsert({ where: { id: 'seed-user-admin' }, create: { id: 'seed-user-admin', phone: '0900000001', email: 'admin@goodgo.vn', fullName: 'Admin GoodGo', role: UserRole.ADMIN, kycStatus: 'VERIFIED', isActive: true, // passwordHash is NULL - cannot login! }, }); ``` ### Enhanced Seed with Passwords - LOGIN ENABLED ```typescript const admin = await prisma.user.create({ data: { id: 'seed-admin-01', phone: '+84900000001', // Normalized phoneHash: computeHmacSha256('+84900000001'), email: 'admin@goodgo.vn', emailHash: computeHmacSha256('admin@goodgo.vn'), passwordHash: await bcrypt.hash('AdminPassword123', 12), fullName: 'Admin GoodGo', role: 'ADMIN', kycStatus: 'VERIFIED', isActive: true, totpEnabled: false, totpBackupCodes: [], }, }); ``` --- ## 10. SUMMARY TABLE | Component | Details | |-----------|---------| | **Password Hashing** | bcrypt, 12 rounds (configurable), min 8 chars | | **Phone Validation** | Vietnamese format, regex with carrier codes | | **Phone Normalization** | `+84XXX...` format (country code) | | **Email Validation** | Basic regex with @ and . | | **Email Normalization** | lowercase, trimmed | | **PII Encryption** | AES-256-GCM (email, phone, kycData) | | **Hash Fields** | HMAC-SHA256 for searchable indexes | | **Backup Codes** | HMAC-SHA256, 10 codes, 8 chars each | | **TOTP** | RFC 6238, 30s period, 6 digits | | **User Roles** | BUYER, SELLER, AGENT, ADMIN | | **Default Active** | true | | **KYC Status** | NONE, PENDING, VERIFIED, REJECTED | --- ## 11. KEY FILES REFERENCE | File | Purpose | |------|---------| | `apps/api/src/modules/auth/domain/value-objects/hashed-password.vo.ts` | Password hashing with bcrypt | | `apps/api/src/modules/auth/domain/value-objects/phone.vo.ts` | Phone validation | | `apps/api/src/modules/auth/domain/value-objects/email.vo.ts` | Email validation | | `apps/api/src/modules/shared/utils/vietnam-phone.validator.ts` | Vietnamese phone regex & normalization | | `apps/api/src/modules/shared/infrastructure/field-encryption.ts` | AES-256-GCM encryption for PII | | `apps/api/src/modules/auth/infrastructure/strategies/local.strategy.ts` | Login flow | | `apps/api/src/modules/auth/infrastructure/services/mfa.service.ts` | TOTP & backup codes | | `apps/api/src/modules/auth/domain/entities/user.entity.ts` | User domain model | | `apps/api/src/modules/auth/infrastructure/repositories/prisma-user.repository.ts` | User persistence | | `scripts/encrypt-pii-fields.ts` | Backfill encryption/hashing script | | `prisma/schema.prisma` | Database schema | | `prisma/seed.ts` | Seed data | --- **Platform:** GoodGo Real Estate Platform (NestJS + Prisma + PostgreSQL 16) **Generated:** April 12, 2026