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>
This commit is contained in:
259
SEED_GENERATION_SCRIPT.ts
Normal file
259
SEED_GENERATION_SCRIPT.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user