/** * GoodGo Platform - Seed User Generation Script * * Creates seed users with full login capability (passwords + PII hashing) * * Usage: * export FIELD_ENCRYPTION_KEY='hex-encoded-32-byte-key' * npx tsx scripts/seed-with-auth.ts */ import crypto from 'node:crypto'; import { PrismaClient, UserRole, type KYCStatus } from '@prisma/client'; import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient(); // ============================================================================ // Configuration // ============================================================================ interface SeedUserConfig { id: string; phone: string; email: string; fullName: string; password: string; role: UserRole; kycStatus: KYCStatus; isActive: boolean; } const SEED_USERS: SeedUserConfig[] = [ { id: 'seed-admin-001', phone: '0900000001', email: 'admin@goodgo.vn', fullName: 'Admin GoodGo', password: 'AdminPassword123', role: UserRole.ADMIN, kycStatus: 'VERIFIED', isActive: true, }, { id: 'seed-agent-001', phone: '0900000002', email: 'agent.nguyen@goodgo.vn', fullName: 'Nguyễn Văn An', password: 'AgentPassword123', role: UserRole.AGENT, kycStatus: 'VERIFIED', isActive: true, }, { id: 'seed-seller-001', phone: '0900000005', email: 'seller.pham@gmail.com', fullName: 'Phạm Đức Dũng', password: 'SellerPassword123', role: UserRole.SELLER, kycStatus: 'VERIFIED', isActive: true, }, { id: 'seed-buyer-001', phone: '0900000004', email: 'buyer.le@gmail.com', fullName: 'Lê Minh Cường', password: 'BuyerPassword123', role: UserRole.BUYER, kycStatus: 'NONE', isActive: true, }, ]; // ============================================================================ // Helper Functions // ============================================================================ /** * Normalize Vietnamese phone number to +84... format */ function normalizeVietnamPhone(phone: string): string { const cleaned = phone.replace(/[\s.-]/g, ''); if (cleaned.startsWith('+84')) return cleaned; if (cleaned.startsWith('84')) return `+${cleaned}`; if (cleaned.startsWith('0')) return `+84${cleaned.slice(1)}`; throw new Error(`Invalid phone format: ${phone}`); } /** * Derive HMAC key from encryption key (same as field-encryption.ts) */ function deriveHmacKey(encryptionKeyHex: string): Buffer { return crypto.hkdfSync( 'sha256', Buffer.from(encryptionKeyHex, 'hex'), Buffer.alloc(0), Buffer.from('goodgo-field-hash', 'utf8'), 32, ) as unknown as Buffer; } /** * Compute HMAC-SHA256 hash for searchable fields */ function computeHash(value: string, hmacKey: Buffer): string { const normalized = value.toLowerCase().trim(); return crypto.createHmac('sha256', hmacKey).update(normalized).digest('hex'); } /** * Hash password with bcrypt */ async function hashPassword(password: string): Promise { if (password.length < 8) { throw new Error('Password must be at least 8 characters'); } return bcrypt.hash(password, 12); } // ============================================================================ // Main Seeding Function // ============================================================================ async function seedUsersWithAuth() { const encryptionKey = process.env['FIELD_ENCRYPTION_KEY']; if (!encryptionKey) { throw new Error('FIELD_ENCRYPTION_KEY environment variable is required'); } const hmacKey = deriveHmacKey(encryptionKey); const stats = { created: 0, skipped: 0, errors: 0, }; console.log('🌱 Seeding users with authentication...\n'); for (const userConfig of SEED_USERS) { try { // Check if user already exists const existing = await prisma.user.findUnique({ where: { id: userConfig.id }, }); if (existing) { console.log(`⏭️ Skipping ${userConfig.fullName} (already exists)`); stats.skipped++; continue; } // 1. Normalize phone const normalizedPhone = normalizeVietnamPhone(userConfig.phone); // 2. Compute hashes const phoneHash = computeHash(normalizedPhone, hmacKey); const emailHash = computeHash(userConfig.email, hmacKey); // 3. Hash password const passwordHash = await hashPassword(userConfig.password); // 4. Create user const user = await prisma.user.create({ data: { id: userConfig.id, phone: normalizedPhone, phoneHash, email: userConfig.email, emailHash, passwordHash, fullName: userConfig.fullName, role: userConfig.role, kycStatus: userConfig.kycStatus, isActive: userConfig.isActive, totpEnabled: false, totpBackupCodes: [], }, }); console.log(`✅ Created ${user.fullName} (${user.role})`); console.log(` 📞 Phone: ${normalizedPhone}`); console.log(` 📧 Email: ${user.email}`); console.log(` 🔑 Can login with password: ${userConfig.password}\n`); stats.created++; } catch (error) { console.error( `❌ Error creating ${userConfig.fullName}:`, error instanceof Error ? error.message : error, ); stats.errors++; } } // Summary console.log('📊 Seed Summary'); console.log(` Created: ${stats.created}`); console.log(` Skipped: ${stats.skipped}`); console.log(` Errors: ${stats.errors}`); if (stats.errors === 0 && stats.created > 0) { console.log('\n✅ Seed completed successfully!'); } } // ============================================================================ // Test Login Function (optional) // ============================================================================ /** * Verify that a created user can actually log in */ async function testLogin(userId: string, password: string): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, }); if (!user || !user.passwordHash) { console.error('User not found or has no password'); return false; } const isValid = await bcrypt.compare(password, user.passwordHash); return isValid; } // ============================================================================ // CLI Entry Point // ============================================================================ async function main() { try { await seedUsersWithAuth(); // Optionally test login const adminUser = SEED_USERS.find((u) => u.role === UserRole.ADMIN); if (adminUser) { console.log('\n🔐 Testing login...'); const loginWorks = await testLogin(adminUser.id, adminUser.password); if (loginWorks) { console.log(`✅ Login test passed for ${adminUser.fullName}`); } else { console.error(`❌ Login test failed for ${adminUser.fullName}`); } } } catch (error) { console.error('Fatal error:', error); process.exit(1); } finally { await prisma.$disconnect(); } } if (require.main === module) { main(); } export { seedUsersWithAuth, testLogin };