Auto-fix 862 lint errors: convert value imports used only as types to `import type`, fix import group ordering in seed.ts and du-an-api.ts, remove unused imports in auth controller, and clean up stale eslint-disable comments referencing non-existent rules. Co-Authored-By: Paperclip <noreply@paperclip.ing>
260 lines
7.0 KiB
TypeScript
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 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<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 };
|