import { PrismaClient, Role, StatusKoas } from '@/prisma/generated/client'; import firebaseAdmin from '../../lib/firebase-admin'; import { UserRecord } from 'firebase-admin/auth'; import { faker } from '@faker-js/faker'; import { UserSeedResult, FirebaseCreateData, KoasData } from '../types/seeding'; import { koasUser } from '../data/users'; // Helper function to generate avatar URL based on gender const getGenderBasedAvatar = (gender?: string): string => { // Default to random gender if not specified const sex = gender?.toLowerCase() === 'female' ? 'female' : 'male'; return faker.image.personPortrait({ sex }); }; // Create a helper function to generate a short bio that won't exceed DB limits const generateShortBio = (): string => { return faker.lorem.sentence(5); }; // Helper function to generate a consistent phone number const generatePhoneNumber = (): string => { return `08${faker.string.numeric(10)}`; }; // Helper function to generate WhatsApp link from phone number const generateWhatsAppLink = (phone: string): string => { const cleanPhone = phone.replace(/[+\s]/g, ''); return `https://wa.me/${cleanPhone}`; }; // Helper function to generate a unique name const generateUniqueName = ( firstName: string, lastName: string, index: number ): string => { return `${firstName} ${lastName}${index > 0 ? ` ${index}` : ''}`; }; export const seedUsers = async ( prisma: PrismaClient, resetFirebase: boolean = false ): Promise => { try { console.log('Starting user seeding process...'); console.log( `Firebase reset option: ${resetFirebase ? 'ENABLED' : 'DISABLED'}` ); // Always delete database records first console.log('Clearing database records...'); // Need to clear related tables first due to foreign key constraints await prisma.user.deleteMany({}); await prisma.notification.deleteMany({}); await prisma.like.deleteMany({}); await prisma.review.deleteMany({}); await prisma.appointment.deleteMany({}); await prisma.timeslot.deleteMany({}); await prisma.schedule.deleteMany({}); await prisma.post.deleteMany({}); await prisma.koasProfile.deleteMany({}); await prisma.pasienProfile.deleteMany({}); await prisma.fasilitatorProfile.deleteMany({}); await prisma.session.deleteMany({}); await prisma.account.deleteMany({}); console.log('All database records cleared'); // Get or create university const universities = await prisma.university.findMany(); let defaultUniversity = universities.length > 0 ? universities[0] : null; if (!defaultUniversity) { defaultUniversity = await prisma.university.create({ data: { name: 'Universitas Negeri Jember', alias: 'UNEJ', location: 'Jl. Kalimantan No.37, Krajan Timur, Sumbersari, Jember', latitude: -8.1649, longitude: 113.7169, image: 'https://upload.wikimedia.org/wikipedia/id/7/70/Logo_Universitas_Jember.png', }, }); console.log('Created default university:', defaultUniversity.name); } const universityId = defaultUniversity.id; const phoneNumbers = new Map(); // Store existing Firebase users if we're not resetting let existingFirebaseUsers: UserRecord[] = []; if (resetFirebase) { // If we're resetting Firebase, we'll delete and recreate all users console.log('Resetting Firebase users...'); try { // Get all current Firebase users const listUsersResult = await firebaseAdmin.auth().listUsers(1000); // Delete all existing Firebase users const deletePromises = listUsersResult.users.map((user) => firebaseAdmin.auth().deleteUser(user.uid) ); await Promise.all(deletePromises); console.log(`Deleted ${listUsersResult.users.length} Firebase users`); } catch (error) { console.error('Error deleting Firebase users:', error); // Continue with creating new users } // Create new users from scratch return await createAllUsersFromScratch(prisma, universityId); } else { // If not resetting Firebase, we'll use existing Firebase users and sync with database console.log('Preserving Firebase users and syncing to database...'); try { // Get all current Firebase users const listUsersResult = await firebaseAdmin.auth().listUsers(1000); existingFirebaseUsers = listUsersResult.users; console.log( `Found ${existingFirebaseUsers.length} existing Firebase users` ); } catch (error) { console.error('Error listing Firebase users:', error); existingFirebaseUsers = []; } // If we have existing Firebase users, create database records for them if (existingFirebaseUsers.length > 0) { return await syncExistingFirebaseUsers( prisma, existingFirebaseUsers, universityId ); } else { // If no existing users, create new ones console.log( 'No existing Firebase users found. Creating from scratch...' ); return await createAllUsersFromScratch(prisma, universityId); } } } catch (error) { console.error('Error seeding users:', error); throw error; } }; // Helper function to create users from scratch async function createAllUsersFromScratch( prisma: PrismaClient, universityId: string ): Promise { console.log('Creating new users from scratch...'); // Create admin user const adminUser = await createAdminUser(prisma); // Set up arrays to store user data const fasilitatorFirebaseUsers: UserRecord[] = []; const koasFirebaseUsers: UserRecord[] = []; const pasienFirebaseUsers: UserRecord[] = []; const koasData: KoasData[] = []; // Create users const fasilitatorCountToCreate = 5; const pasienCountToCreate = 10; await createFasilitators(fasilitatorCountToCreate, fasilitatorFirebaseUsers); await createKoasFromData(koasFirebaseUsers, koasData); // Ganti ke fungsi baru await createPatients(pasienCountToCreate, pasienFirebaseUsers); // Create database records await createFasilitatorRecords(prisma, fasilitatorFirebaseUsers); await createKoasRecords(prisma, koasFirebaseUsers, koasData, universityId); await createPatientRecords(prisma, pasienFirebaseUsers); console.log('User creation complete!'); console.log( `Created ${fasilitatorFirebaseUsers.length} facilitators, ${koasFirebaseUsers.length} koas, and ${pasienFirebaseUsers.length} patient accounts` ); return { admin: adminUser, fasilitatorCount: fasilitatorFirebaseUsers.length, koasCount: koasFirebaseUsers.length, pasienCount: pasienFirebaseUsers.length, }; } // Create admin user if none exists async function createAdminUser(prisma: PrismaClient) { let adminFirebase; try { // Check if admin already exists adminFirebase = await firebaseAdmin .auth() .getUserByEmail('admin@dentakoas.com'); console.log('Admin user exists in Firebase'); } catch (error) { // Create new admin if not exists adminFirebase = await firebaseAdmin.auth().createUser({ email: 'admin@dentakoas.com', password: 'Password123', displayName: 'Admin User', photoURL: getGenderBasedAvatar('male'), // Default to male for admin emailVerified: true, }); await firebaseAdmin .auth() .setCustomUserClaims(adminFirebase.uid, { role: 'Admin' }); console.log('Created new admin user in Firebase'); } // Create admin record in database const phoneNumber = generatePhoneNumber(); const adminUser = await prisma.user.create({ data: { id: adminFirebase.uid, givenName: 'Admin', familyName: 'User', name: 'Admin User', email: 'admin@dentakoas.com', password: adminFirebase.passwordHash, role: 'Admin', image: adminFirebase.photoURL || getGenderBasedAvatar('male'), phone: phoneNumber, address: 'Jl. Kalimantan No.37, Jember', emailVerified: new Date(), }, }); console.log('Created admin record in database'); return adminUser; } // Create admin from existing Firebase user async function createExistingAdminRecord( prisma: PrismaClient, adminFirebase: UserRecord ) { const phoneNumber = generatePhoneNumber(); const adminUser = await prisma.user.create({ data: { id: adminFirebase.uid, givenName: 'Admin', familyName: 'User', name: adminFirebase.displayName || 'Admin User', email: adminFirebase.email || 'admin@dentakoas.com', password: adminFirebase.passwordHash, role: 'Admin', image: adminFirebase.photoURL || getGenderBasedAvatar('male'), phone: phoneNumber, address: 'Jl. Kalimantan No.37, Jember', emailVerified: new Date(), }, }); console.log('Created admin record in database from existing Firebase user'); return adminUser; } // Create facilitator users in Firebase async function createFasilitators(count: number, userList: UserRecord[]) { for (let i = 0; i < count; i++) { // Determine gender randomly const gender = faker.helpers.arrayElement(['male', 'female']); const firstName = faker.person.firstName(gender as any); const lastName = faker.person.lastName(); const email = faker.internet .email({ firstName, lastName, provider: 'unej.ac.id' }) .toLowerCase(); // Add index to name to ensure uniqueness const displayName = generateUniqueName(`Dr. ${firstName}`, lastName, i); const firebaseUser = await firebaseAdmin.auth().createUser({ email: email, password: 'Password123', displayName: displayName, photoURL: getGenderBasedAvatar(gender), emailVerified: true, }); await firebaseAdmin .auth() .setCustomUserClaims(firebaseUser.uid, { role: 'Fasilitator', gender }); userList.push(firebaseUser); } } // Create koas users in Firebase async function createKoasFromData( userList: UserRecord[], koasDataList: KoasData[] ) { // Cek email duplikat dan skip jika sudah pernah diproses const emailSet = new Set(); for (const koas of koasUser) { const email = koas.email.trim().toLowerCase(); if (emailSet.has(email)) { console.warn( `SKIP: Duplicate email detected for ${koas.nama} (${email})` ); continue; } emailSet.add(email); // Normalisasi gender const gender = koas.gender?.toLowerCase().includes('laki') ? 'male' : 'female'; const displayName = koas.nama; const koasNumber = koas.koas_number; const entryYear = typeof koas.entry_year === 'number' ? koas.entry_year : 2023; const photoURL = koas.img; const phone = koas.phone; try { const firebaseUser = await firebaseAdmin.auth().createUser({ email: email, password: 'Password123', displayName: displayName, photoURL: photoURL || getGenderBasedAvatar(gender), emailVerified: true, }); await firebaseAdmin .auth() .setCustomUserClaims(firebaseUser.uid, { role: 'Koas', gender }); userList.push(firebaseUser); koasDataList.push({ id: firebaseUser.uid, koasNumber: koasNumber, entryYear: entryYear, gender: gender, phone: phone, }); } catch (err) { console.error( `Error creating Firebase user for koas: ${displayName} (${email})` ); throw err; } } } // Create patient users in Firebase async function createPatients(count: number, userList: UserRecord[]) { const usedNames = new Set(); for (let i = 0; i < count; i++) { // Determine gender randomly const gender = faker.helpers.arrayElement(['male', 'female']); const firstName = faker.person.firstName(gender as any); const lastName = faker.person.lastName(); // Create a unique name with index suffix if needed let displayName = `${firstName} ${lastName}`; let nameIndex = 0; // If name already exists, add a number suffix while (usedNames.has(displayName.toLowerCase())) { nameIndex++; displayName = generateUniqueName(firstName, lastName, nameIndex); } usedNames.add(displayName.toLowerCase()); const email = faker.internet.email({ firstName, lastName }).toLowerCase(); const firebaseUser = await firebaseAdmin.auth().createUser({ email: email, password: 'Password123', displayName: displayName, photoURL: getGenderBasedAvatar(gender), emailVerified: true, }); await firebaseAdmin .auth() .setCustomUserClaims(firebaseUser.uid, { role: 'Pasien', gender }); userList.push(firebaseUser); } } // Helper function to sync existing Firebase users to database async function syncExistingFirebaseUsers( prisma: PrismaClient, existingFirebaseUsers: UserRecord[], universityId: string ): Promise { console.log('Syncing existing Firebase users to database...'); // Track used names to ensure uniqueness const usedNames = new Set(); // Group users by role (from custom claims) const adminUsers: UserRecord[] = []; const fasilitatorUsers: UserRecord[] = []; const koasUsers: UserRecord[] = []; const pasienUsers: UserRecord[] = []; const koasData: KoasData[] = []; // First pass to collect all existing names for (const user of existingFirebaseUsers) { if (user.displayName) { usedNames.add(user.displayName.toLowerCase()); } } // Process users by role for (let user of existingFirebaseUsers) { const customClaims = user.customClaims as | { role?: string; gender?: string } | undefined; const role = customClaims?.role || 'Pasien'; // Ensure user has a unique display name if (!user.displayName || user.displayName.trim() === '') { // Generate a display name if none exists const nameParts = user.email?.split('@')[0].split('.') || ['User']; const firstName = nameParts[0]?.charAt(0).toUpperCase() + nameParts[0]?.slice(1) || 'User'; const lastName = nameParts[1]?.charAt(0).toUpperCase() + nameParts[1]?.slice(1) || ''; // Create a base display name let displayName = `${firstName}${lastName ? ' ' + lastName : ''}`; let nameIndex = 0; // Ensure uniqueness while (usedNames.has(displayName.toLowerCase())) { nameIndex++; displayName = `${firstName}${ lastName ? ' ' + lastName : '' } ${nameIndex}`; } usedNames.add(displayName.toLowerCase()); // Update Firebase user with the new display name await firebaseAdmin.auth().updateUser(user.uid, { displayName: displayName, }); // Update our local user record user = await firebaseAdmin.auth().getUser(user.uid); } switch (role) { case 'Admin': adminUsers.push(user); break; case 'Fasilitator': fasilitatorUsers.push(user); break; case 'Koas': koasUsers.push(user); // Generate koas number if syncing existing users const entryYear = faker.helpers.arrayElement([23, 24]); const koasNumber = `${entryYear}1611${faker.string.numeric(6)}`; koasData.push({ id: user.uid, koasNumber: koasNumber, entryYear: entryYear, }); break; default: // Pasien or unknown roles pasienUsers.push(user); break; } } console.log( `Categorized users: ${adminUsers.length} admins, ${fasilitatorUsers.length} facilitators, ${koasUsers.length} koas, ${pasienUsers.length} patients` ); // Create database records for each user type let adminUser = null; if (adminUsers.length > 0) { adminUser = await createExistingAdminRecord(prisma, adminUsers[0]); } else { // Create admin if none exists adminUser = await createAdminUser(prisma); } await createFasilitatorRecords(prisma, fasilitatorUsers); await createKoasRecords(prisma, koasUsers, koasData, universityId); await createPatientRecords(prisma, pasienUsers); console.log('User synchronization complete!'); return { admin: adminUser, fasilitatorCount: fasilitatorUsers.length, koasCount: koasUsers.length, pasienCount: pasienUsers.length, }; } // Create facilitator records in database async function createFasilitatorRecords( prisma: PrismaClient, users: UserRecord[] ) { // Skip if no users if (users.length === 0) return; // Create user records one by one to handle potential duplicates for (const fbUser of users) { try { // Determine gender from claims const customClaims = fbUser.customClaims as | { gender?: string } | undefined; const gender = customClaims?.gender || 'male'; const phoneNumber = generatePhoneNumber(); await prisma.user.create({ data: { id: fbUser.uid, givenName: fbUser.displayName?.split(' ')[1] || '', // Skip "Dr." prefix familyName: fbUser.displayName?.split(' ').slice(2).join(' ') || '', name: fbUser.displayName || 'User' + fbUser.uid.substring(0, 6), // Ensure uniqueness email: fbUser.email || '', emailVerified: new Date(), password: fbUser.passwordHash, role: 'Fasilitator', image: fbUser.photoURL || getGenderBasedAvatar(gender), phone: phoneNumber, address: faker.location.streetAddress(), }, }); // Create facilitator profile await prisma.fasilitatorProfile.create({ data: { userId: fbUser.uid, university: 'Universitas Negeri Jember', }, }); } catch (error) { console.error( `Error creating Fasilitator record for ${fbUser.uid}:`, error ); } } } // Create koas records in database async function createKoasRecords( prisma: PrismaClient, users: UserRecord[], koasData: KoasData[], universityId: string ) { if (users.length === 0) return; for (const fbUser of users) { try { // Ambil data koas dari koasData const koasInfo = koasData.find((k) => k.id === fbUser.uid); // Fallback jika tidak ada const gender = koasInfo?.gender; const phone = koasInfo?.phone; const koasNumber = koasInfo?.koasNumber || ''; const entryYear = koasInfo?.entryYear || 2023; const nama = fbUser.displayName!; // Split nama untuk givenName dan familyName const nameParts = nama.split(' '); const givenName = nameParts[0] || ''; const familyName = nameParts.slice(1).join(' ') || ''; // Insert ke tabel User await prisma.user.create({ data: { id: fbUser.uid, givenName: givenName, familyName: familyName, name: nama, email: fbUser.email || '', emailVerified: new Date(), password: fbUser.passwordHash, role: 'Koas', phone: phone, address: faker.location.streetAddress(), }, }); // Insert ke tabel KoasProfile await prisma.koasProfile.create({ data: { userId: fbUser.uid, koasNumber: koasNumber, age: entryYear ? entryYear.toString() : undefined, gender: gender === 'female' ? 'Female' : 'Male', departement: 'Profesi Dokter Gigi', university: 'Universitas Negeri Jember', universityId: universityId, bio: generateShortBio(), whatsappLink: phone ? generateWhatsAppLink(phone) : '', status: 'Approved' as StatusKoas, experience: faker.helpers.rangeToNumber({ min: 0, max: 5 }), }, }); } catch (error) { console.error(`Error creating Koas record for ${fbUser.uid}:`, error); } } } // Create patient records in database async function createPatientRecords(prisma: PrismaClient, users: UserRecord[]) { // Skip if no users if (users.length === 0) return; // Create user records one by one to handle potential duplicates for (const fbUser of users) { try { // Extract gender from claims const customClaims = fbUser.customClaims as | { gender?: string } | undefined; const gender = customClaims?.gender || 'male'; const phoneNumber = generatePhoneNumber(); // Create user record await prisma.user.create({ data: { id: fbUser.uid, givenName: fbUser.displayName?.split(' ')[0] || '', familyName: fbUser.displayName?.split(' ').slice(1).join(' ') || '', name: fbUser.displayName || 'User' + fbUser.uid.substring(0, 6), // Ensure uniqueness email: fbUser.email || '', emailVerified: new Date(), password: '$2a$10$Ab6y1GO/BF.MMuZ97zj9B.S2s8wVEMRFXDn8GK9tmlpHt.T2HzYHW', role: 'Pasien', image: fbUser.photoURL || getGenderBasedAvatar(gender), phone: phoneNumber, address: faker.location.streetAddress(), }, }); // Create patient profile await prisma.pasienProfile.create({ data: { userId: fbUser.uid, age: faker.helpers.rangeToNumber({ min: 18, max: 70 }).toString(), gender: gender === 'female' ? 'Female' : 'Male', bio: generateShortBio(), }, }); } catch (error) { console.error(`Error creating Pasien record for ${fbUser.uid}:`, error); } } } // Helper function to generate a hash from a string (for consistent avatar IDs) function hashString(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash); }