# 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<Result<HashedPassword, string>> {
  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<boolean> {
  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<Email, string> {
  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
