Root now contains only essential files: README.md, CLAUDE.md, CHANGELOG.md, CONTRIBUTING.md Reorganized into: docs/audits/ — all audit reports & checklists (71 files) docs/architecture/ — codebase overview, implementation plan docs/guides/ — auth guide, implementation checklist docs/load-testing/ — k6 load test guides & endpoints docs/security/ — payment & security reviews Also removed 5 untracked debug/investigation files and cleaned up playwright-report/ & test-results/ artifacts. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
11 KiB
11 KiB
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_ROUNDSenv var (default:12) - Min Password Length: 8 characters
Key Code
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
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
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 inemailHash(HMAC-SHA256)phone→ encrypted, with hash inphoneHash(HMAC-SHA256)kycData→ encrypted (no separate hash)
Hash Computation
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
- Validate phone format - normalize Vietnamese phone
- Find user by phoneHash - lookup in
phoneHashunique index - Check user status -
user.isActivemust be true - Compare password - bcrypt.compare(plainPassword, passwordHash)
- Check MFA - if
user.totpEnabledis true, return MFA challenge - Issue tokens - if no MFA, generate JWT pair
Login Response (No MFA)
{
"requiresMfa": false,
"tokens": {
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc...",
"expiresIn": 3600
}
}
Login Response (MFA Required)
{
"requiresMfa": true,
"challengeId": "challenge-id-here"
}
6. USER ROLES
UserRole Enum
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
- Valid phone - Must pass Vietnamese phone validation
- Valid email - Must pass email regex
- Hashed password - Must use bcrypt with ≥12 rounds
- Active status -
isActive: true - Normalized phone - Stored in
+84...format - Hash fields - Must populate
phoneHashandemailHashfor queries
Node.js/TypeScript Script
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
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: falsefor simplicity - Set
totpSecret: null - Set
totpBackupCodes: []
9. SEED USER EXAMPLE
Current Seed (from prisma/seed.ts) - NO LOGIN
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
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