Files
goodgo-platform/SEED_GENERATION_SCRIPT.ts
Ho Ngoc Hai 25420720e7 fix(api,ci): remove type-only imports for DI and isolate CI ports from dev
- Remove `type` keyword from NestJS injectable class imports across all
  modules to fix runtime DI resolution (330+ handler/listener files)
- Offset CI docker-compose ports (5433/6380/8109/9002) to avoid
  conflicts with running dev containers
- Update .env.test, playwright.config.ts, and e2e workflow to use
  isolated CI ports with configurable overrides
- Fix prisma/seed.ts to use deterministic IDs for Prisma 7 upsert
  compatibility (phoneHash replaced phone as unique index)
- Add dedicated Docker bridge network for CI service containers

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-04-13 01:40:14 +07:00

260 lines
7.0 KiB
TypeScript

/**
* 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 * as bcrypt from 'bcrypt';
import crypto from 'node:crypto';
import { PrismaClient, UserRole, KYCStatus } from '@prisma/client';
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<string> {
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<boolean> {
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 };