import 'dotenv/config' // @ts-ignore - bcryptjs ships its own types in this repo setup import bcrypt from 'bcryptjs' import { prisma } from '../src/db/prisma.js' type SeedRole = 'admin' | 'user' type SeedUserStatus = 'active' | 'banned' type SeedHostStatus = 'online' | 'offline' | 'maintenance' type SeedHostInstanceType = 'container' | 'vm' | 'both' type SeedPackageInstanceType = 'container' | 'vm' type SeedNetworkMode = 'nat' | 'nat_ipv6' | 'nat_ipv6_nat' | 'ipv6_only' | 'ipv6_nat' type SeedInstanceStatus = 'running' | 'stopped' | 'suspended' | 'error' type SeedTrafficStatus = 'NORMAL' | 'WARNING' | 'LIMITED' type SeedProtocol = 'tcp' | 'udp' type SeedProxyStatus = 'pending' | 'active' | 'error' type SeedBackupStatus = 'ready' | 'error' type SeedTicketPriority = 'low' | 'normal' | 'high' | 'urgent' type SeedTicketStatus = 'open' | 'in_progress' | 'resolved' | 'closed' type SeedBalanceLogType = 'recharge' | 'consume' | 'gift' | 'admin_adjust' type SeedNotificationChannelType = 'telegram' | 'discord' | 'email' | 'webhook' type SeedAnnouncementType = 'system_broadcast' | 'host_broadcast' | 'admin_message' | 'host_message' type SeedPaymentProviderType = 'yipay' | 'stripe' | 'alipay_direct' | 'wechat_direct' | 'manual' type SeedRechargeStatus = 'pending' | 'paid' | 'completed' | 'failed' | 'cancelled' | 'refunded' type SeedAffLogType = 'new_purchase' | 'renew' | 'convert' type SeedAffWithdrawalStatus = 'pending' | 'approved' | 'rejected' type SeedRedeemCodeType = 'c' | 'r' | 'd' | 't' | 'p' type SeedQuota = { hostLimit: number packageLimit: number monthlyTrafficLimit: bigint | null } type SeedUserSpec = { username: string email: string role: SeedRole status: SeedUserStatus banReason: string | null avatarStyle: string quota: SeedQuota } type SeedHostSpec = { ownerUsername: string name: string url: string location: string countryCode: string architecture: 'x86_64' | 'aarch64' status: SeedHostStatus instanceType: SeedHostInstanceType natPublicIp: string natPublicIpv6: string natBindIp: string natBindIpv6: string natPortStart: number natPortEnd: number ipAddress: string ipv6Subnet: string ipv6Gateway: string tags: string[] storageSize: number cpuAllowanceMax: number memoryMax: number caddyEnabled: boolean } type SeedStoragePoolSpec = { name: string driver: string purpose: 'instance_data' | 'instance_storage' description: string } type SeedPlanSpec = { name: string description: string cpu: number memory: number disk: number portLimit: number snapshotLimit: number backupLimit: number siteLimit: number swapSize: number trafficLimit: bigint trafficLimitSpeed: string price: number billingCycle: number setupFee: number isActive: boolean sortOrder: number slaGuarantee: number | null } type SeedPackageSpec = { key: string ownerUsername: string name: string description: string hostNames: string[] cpuMax: number memoryMax: number diskMax: number bandwidthMax: number networkMode: SeedNetworkMode instanceType: SeedPackageInstanceType privileged: boolean nested: boolean active: boolean portLimit: number snapshotLimit: number backupLimit: number siteLimit: number monthlyTrafficLimit: bigint ioLimitMode: 'throughput' | 'iops' limitsRead: string limitsWrite: string limitsReadIops: number limitsWriteIops: number limitsIngress: string limitsEgress: string limitsProcesses: number limitsCpuPriority: number bootAutostart: boolean bootAutostartPriority: number bootAutostartDelay: number bootHostShutdownTimeout: number globalShared: boolean globalQuotaMultiplier: number | null globalMaxInstances: number | null allowInstanceDeletion: boolean plans: SeedPlanSpec[] } type GeneratedPackageRecord = { id: number spec: SeedPackageSpec planIdsByName: Map } type SeedInstanceSpec = { name: string incusId: string userUsername: string packageKey: string planName: string hostName: string image: string status: SeedInstanceStatus createdAt: Date expiresAt: Date | null suspendedAt: Date | null suspendedBy: number | null suspendReason: string | null autoRenew: boolean cloudInitState: string | null cloudInitSource: string | null ipv4: string | null ipv6: string | null sshPort: number | null monthlyTrafficUsed: bigint trafficStatus: SeedTrafficStatus lastIpv6ReassignAt: Date | null lastPlanChangeAt: Date | null lastSyncedAt: Date } type SeedBalanceLogSpec = { userId: number type: SeedBalanceLogType amount: number balanceBefore: number balanceAfter: number orderId: string | null remark: string } type GeneratedInstanceRecord = { id: number userId: number hostId: number packageId: number packagePlanId: number spec: SeedInstanceSpec packageSpec: SeedPackageSpec plan: SeedPlanSpec } type CreatedNotificationChannel = { id: number userId: number type: SeedNotificationChannelType } type CreatedAffCode = { id: number userId: number packagePlanId: number | null code: string } const SEED_PREFIX = 'bulkseed' const SEED_MARK = `[${SEED_PREFIX}]` const SEED_CODE_PREFIX = SEED_PREFIX.toUpperCase() const SHARED_PASSWORD = 'BulkSeed123!' const LOCAL_PASSWORD_SALT = '$2a$10$1234567890123456789012' const TODAY = new Date() const avatarStyles = ['bigSmile', 'croodles', 'notionists', 'lorelei'] const countries = ['us', 'sg', 'jp', 'de', 'nl', 'au'] const cities = ['Los Angeles', 'Singapore', 'Tokyo', 'Frankfurt', 'Amsterdam', 'Sydney'] const containerImages = [ 'images:ubuntu/24.04/cloud', 'images:debian/12/cloud', 'images:almalinux/9/cloud', 'images:rockylinux/9/cloud' ] const vmImages = [ 'images:ubuntu/24.04/cloud', 'images:debian/12/cloud', 'images:almalinux/9/cloud' ] const redeemCodeTypes: SeedRedeemCodeType[] = ['c', 'r', 'd', 't', 'p'] const ownerUsers: SeedUserSpec[] = [ createOwnerUser(1, 'admin', 'admin', 24, 40), createOwnerUser(2, 'apac', 'user', 18, 32), createOwnerUser(3, 'eu', 'user', 18, 32), createOwnerUser(4, 'us', 'user', 18, 32), createOwnerUser(5, 'edge', 'user', 18, 32), createOwnerUser(6, 'lab', 'user', 18, 32) ] const customerUsers: SeedUserSpec[] = Array.from({ length: 30 }, (_, index) => { const id = index + 1 const status: SeedUserStatus = id % 17 === 0 ? 'banned' : 'active' return { username: `${SEED_PREFIX}-user-${pad(id, 2)}`, email: `${SEED_PREFIX}-user-${pad(id, 2)}@incudal.local`, role: 'user', status, banReason: status === 'banned' ? `${SEED_MARK} banned sample user` : null, avatarStyle: avatarStyles[index % avatarStyles.length], quota: { hostLimit: 0, packageLimit: 0, monthlyTrafficLimit: bytesFromGb(800) } } }) const allSeedUsers = [...ownerUsers, ...customerUsers] const hostSpecs: SeedHostSpec[] = [ createHostSpec(1, ownerUsers[0].username, 'official-us-west', 'Los Angeles, US', 'us', 'x86_64', 'both', 'online'), createHostSpec(2, ownerUsers[0].username, 'official-eu-vm', 'Frankfurt, DE', 'de', 'x86_64', 'vm', 'online'), createHostSpec(3, ownerUsers[1].username, 'apac-sg-edge', 'Singapore, SG', 'sg', 'x86_64', 'both', 'online'), createHostSpec(4, ownerUsers[1].username, 'apac-tokyo-arm', 'Tokyo, JP', 'jp', 'aarch64', 'container', 'online'), createHostSpec(5, ownerUsers[2].username, 'eu-amsterdam', 'Amsterdam, NL', 'nl', 'x86_64', 'both', 'online'), createHostSpec(6, ownerUsers[2].username, 'eu-madrid-vm', 'Madrid, ES', 'us', 'x86_64', 'vm', 'online'), createHostSpec(7, ownerUsers[3].username, 'us-ashburn', 'Ashburn, US', 'us', 'x86_64', 'both', 'online'), createHostSpec(8, ownerUsers[3].username, 'us-dallas-container', 'Dallas, US', 'us', 'x86_64', 'container', 'online'), createHostSpec(9, ownerUsers[4].username, 'edge-seoul', 'Seoul, KR', 'jp', 'aarch64', 'container', 'online'), createHostSpec(10, ownerUsers[4].username, 'edge-sydney', 'Sydney, AU', 'au', 'x86_64', 'both', 'online'), createHostSpec(11, ownerUsers[5].username, 'lab-london', 'London, GB', 'de', 'x86_64', 'both', 'online'), createHostSpec(12, ownerUsers[5].username, 'lab-warsaw-vm', 'Warsaw, PL', 'nl', 'x86_64', 'vm', 'online') ] const storagePools: SeedStoragePoolSpec[] = [ { name: 'default', driver: 'zfs', purpose: 'instance_data', description: `${SEED_MARK} default pool` }, { name: 'archive', driver: 'dir', purpose: 'instance_storage', description: `${SEED_MARK} archive pool` } ] const packageSpecs = buildPackageSpecs() const instanceSpecs = buildInstanceSpecs(packageSpecs) function createOwnerUser(index: number, slug: string, role: SeedRole, hostLimit: number, packageLimit: number): SeedUserSpec { return { username: `${SEED_PREFIX}-owner-${slug}`, email: `${SEED_PREFIX}-owner-${slug}@incudal.local`, role, status: 'active', banReason: null, avatarStyle: avatarStyles[(index - 1) % avatarStyles.length], quota: { hostLimit, packageLimit, monthlyTrafficLimit: bytesFromGb(3000) } } } function createHostSpec( index: number, ownerUsername: string, slug: string, location: string, countryCode: string, architecture: 'x86_64' | 'aarch64', instanceType: SeedHostInstanceType, status: SeedHostStatus ): SeedHostSpec { const natBase = 40 + index const hex = (0x40 + index).toString(16) return { ownerUsername, name: `${SEED_PREFIX}-host-${pad(index, 2)}-${slug}`, url: `https://${SEED_PREFIX}-host-${pad(index, 2)}-${slug}.local`, location, countryCode, architecture, status, instanceType, natPublicIp: `198.51.100.${natBase}`, natPublicIpv6: `2001:db8:feed:${hex}::${index}`, natBindIp: `10.255.${index}.10`, natBindIpv6: `fd00:255:${hex}::10`, natPortStart: 20000 + (index - 1) * 400, natPortEnd: 20000 + (index - 1) * 400 + 399, ipAddress: `10.20.${index}.10`, ipv6Subnet: `2001:db8:${hex}::/64`, ipv6Gateway: `2001:db8:${hex}::1`, tags: [SEED_PREFIX, slug, architecture, instanceType], storageSize: 200 + index * 15, cpuAllowanceMax: 2200 + index * 180, memoryMax: 65536 + index * 4096, caddyEnabled: index % 2 === 0 } } function bytesFromGb(gb: number): bigint { return BigInt(gb) * 1024n * 1024n * 1024n } function pad(value: number, digits: number): string { return String(value).padStart(digits, '0') } function daysAgo(days: number, hour = 10): Date { const date = new Date(TODAY) date.setUTCDate(date.getUTCDate() - days) date.setUTCHours(hour, 0, 0, 0) return date } function daysFromNow(days: number, hour = 10): Date { const date = new Date(TODAY) date.setUTCDate(date.getUTCDate() + days) date.setUTCHours(hour, 0, 0, 0) return date } function addMonths(input: Date, months: number): Date { const output = new Date(input) output.setUTCMonth(output.getUTCMonth() + months) return output } function roundToStep(value: number, step: number): number { return Math.max(step, Math.round(value / step) * step) } function getBaseRechargeAmount(user: SeedUserSpec, index: number, spend: number): number { return Math.max( user.role === 'admin' ? 30000 + index * 500 : 9000 + index * 120, spend + 2000 + index * 50 ) } function getGiftAmount(user: SeedUserSpec, index: number): number { return user.role === 'admin' ? 1500 : 300 + index * 10 } function getSeedOrderNo(index: number): string { return `${SEED_PREFIX}-recharge-${pad(index + 1, 3)}` } function buildDemoPublicKey(username: string, index: number): string { const body = Buffer.from(`${SEED_PREFIX}:${username}:${index}`).toString('base64') return `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI${body.slice(0, 40)} ${username}@seed.local` } function buildDemoFingerprint(username: string, index: number): string { const body = Buffer.from(`${username}:${index}:${SEED_PREFIX}`).toString('base64') return `SHA256:${body.slice(0, 32)}` } function getOwnerHosts(ownerUsername: string): SeedHostSpec[] { return hostSpecs.filter(host => host.ownerUsername === ownerUsername) } function buildPackageSpecs(): SeedPackageSpec[] { const thirdModes: SeedNetworkMode[] = ['nat_ipv6_nat', 'ipv6_only', 'ipv6_nat'] const packageList: SeedPackageSpec[] = [] ownerUsers.forEach((owner, ownerIndex) => { const hosts = getOwnerHosts(owner.username) const containerHosts = hosts.filter(host => host.instanceType !== 'vm') const vmHosts = hosts.filter(host => host.instanceType !== 'container') const cpuBase = 220 + ownerIndex * 20 const memoryBase = 8192 + ownerIndex * 1024 const diskBase = 102400 + ownerIndex * 10240 const thirdMode = thirdModes[ownerIndex % thirdModes.length] packageList.push( createPackageSpec({ index: packageList.length + 1, ownerUsername: owner.username, suffix: 'nat-box', title: `NAT Box ${ownerIndex + 1}`, description: `${SEED_MARK} public IPv4 NAT container package`, hostNames: uniqueNames(containerHosts.map(host => host.name)), networkMode: 'nat', instanceType: 'container', cpuMax: cpuBase, memoryMax: memoryBase, diskMax: diskBase, bandwidthMax: 1200 + ownerIndex * 120, globalShared: true, portLimit: 24, snapshotLimit: 5, backupLimit: 3, siteLimit: 4, monthlyTrafficLimit: bytesFromGb(480 + ownerIndex * 40), ioLimitMode: ownerIndex % 2 === 0 ? 'throughput' : 'iops', globalQuotaMultiplier: 1.5 }), createPackageSpec({ index: packageList.length + 2, ownerUsername: owner.username, suffix: 'dual-stack', title: `Dual Stack ${ownerIndex + 1}`, description: `${SEED_MARK} IPv4 NAT + routed IPv6 package`, hostNames: uniqueNames(containerHosts.map(host => host.name)), networkMode: 'nat_ipv6', instanceType: 'container', cpuMax: cpuBase + 30, memoryMax: memoryBase + 1024, diskMax: diskBase + 10240, bandwidthMax: 1400 + ownerIndex * 120, globalShared: ownerIndex % 2 === 0, portLimit: 28, snapshotLimit: 6, backupLimit: 3, siteLimit: 5, monthlyTrafficLimit: bytesFromGb(560 + ownerIndex * 40), ioLimitMode: 'throughput', globalQuotaMultiplier: 1.8 }), createPackageSpec({ index: packageList.length + 3, ownerUsername: owner.username, suffix: `ipv6-${thirdMode}`, title: `IPv6 Mix ${ownerIndex + 1}`, description: `${SEED_MARK} IPv6-heavy container package`, hostNames: uniqueNames(containerHosts.map(host => host.name)), networkMode: thirdMode, instanceType: 'container', cpuMax: cpuBase + 10, memoryMax: memoryBase + 512, diskMax: diskBase, bandwidthMax: 1000 + ownerIndex * 100, globalShared: true, portLimit: thirdMode === 'ipv6_only' ? 0 : 18, snapshotLimit: 4, backupLimit: 2, siteLimit: 4, monthlyTrafficLimit: bytesFromGb(360 + ownerIndex * 30), ioLimitMode: 'throughput', globalQuotaMultiplier: 1.2 }), createPackageSpec({ index: packageList.length + 4, ownerUsername: owner.username, suffix: 'vm-flex', title: `VM Flex ${ownerIndex + 1}`, description: `${SEED_MARK} KVM package for billing and lifecycle screens`, hostNames: uniqueNames(vmHosts.map(host => host.name)), networkMode: ownerIndex % 2 === 0 ? 'nat_ipv6' : 'nat', instanceType: 'vm', cpuMax: 300 + ownerIndex * 30, memoryMax: 12288 + ownerIndex * 1024, diskMax: 153600 + ownerIndex * 10240, bandwidthMax: 1800 + ownerIndex * 100, globalShared: ownerIndex % 3 !== 0, portLimit: 10, snapshotLimit: 4, backupLimit: 3, siteLimit: 1, monthlyTrafficLimit: bytesFromGb(700 + ownerIndex * 50), ioLimitMode: 'iops', globalQuotaMultiplier: 1.0 }) ) }) return packageList } function createPackageSpec(input: { index: number ownerUsername: string suffix: string title: string description: string hostNames: string[] networkMode: SeedNetworkMode instanceType: SeedPackageInstanceType cpuMax: number memoryMax: number diskMax: number bandwidthMax: number globalShared: boolean portLimit: number snapshotLimit: number backupLimit: number siteLimit: number monthlyTrafficLimit: bigint ioLimitMode: 'throughput' | 'iops' globalQuotaMultiplier: number | null }): SeedPackageSpec { const isVm = input.instanceType === 'vm' const key = `${SEED_PREFIX}-pkg-${pad(input.index, 2)}` const planNames = isVm ? ['Starter', 'Business', 'Scale'] : ['Nano', 'Plus', 'Prime'] const planScales = isVm ? [0.28, 0.46, 0.68] : [0.18, 0.36, 0.58] const plans = planNames.map((planName, index) => { const scale = planScales[index] const baseCpu = roundToStep(input.cpuMax * scale, isVm ? 20 : 5) const baseMemory = roundToStep(input.memoryMax * scale, 256) const baseDisk = roundToStep(input.diskMax * scale, 1024) return { name: planName, description: `${SEED_MARK} ${planName} plan for ${input.title}`, cpu: baseCpu, memory: baseMemory, disk: baseDisk, portLimit: input.networkMode === 'ipv6_only' ? 0 : Math.max(0, Math.min(input.portLimit, 6 + index * (isVm ? 2 : 6))), snapshotLimit: Math.min(input.snapshotLimit, 2 + index), backupLimit: Math.min(input.backupLimit, 1 + Math.floor(index / 1)), siteLimit: isVm ? Math.min(input.siteLimit, index === 2 ? 1 : 0) : Math.min(input.siteLimit, 1 + index * 2), swapSize: isVm ? 0 : index === 0 ? 0 : index * 512, trafficLimit: bytesFromGb((isVm ? 180 : 90) + index * (isVm ? 160 : 80) + input.index * 5), trafficLimitSpeed: ['2Mbit', '5Mbit', '10Mbit'][index], price: (isVm ? 2990 : 890) + input.index * (isVm ? 60 : 25) + index * (isVm ? 1800 : 900), billingCycle: index === 2 && isVm ? 3 : 1, setupFee: isVm ? index * 300 : 0, isActive: true, sortOrder: (index + 1) * 10, slaGuarantee: [99.5, 99.9, 99.95][index] } }) return { key, ownerUsername: input.ownerUsername, name: `${key}-${input.suffix}`, description: input.description, hostNames: input.hostNames, networkMode: input.networkMode, instanceType: input.instanceType, cpuMax: input.cpuMax, memoryMax: input.memoryMax, diskMax: input.diskMax, bandwidthMax: input.bandwidthMax, privileged: !isVm, nested: !isVm, active: true, portLimit: input.portLimit, snapshotLimit: input.snapshotLimit, backupLimit: input.backupLimit, siteLimit: input.siteLimit, monthlyTrafficLimit: input.monthlyTrafficLimit, ioLimitMode: input.ioLimitMode, limitsRead: isVm ? '350MB' : '220MB', limitsWrite: isVm ? '350MB' : '220MB', limitsReadIops: isVm ? 2200 : 1100, limitsWriteIops: isVm ? 2200 : 1100, limitsIngress: isVm ? '1Gbit' : '600Mbit', limitsEgress: isVm ? '1Gbit' : '600Mbit', limitsProcesses: isVm ? 800 : 1200, limitsCpuPriority: isVm ? 8 : 10, bootAutostart: true, bootAutostartPriority: 10 + (input.index % 10), bootAutostartDelay: 10 + (input.index % 5) * 5, bootHostShutdownTimeout: isVm ? 60 : 45, globalShared: input.globalShared, globalQuotaMultiplier: input.globalQuotaMultiplier, globalMaxInstances: 12, allowInstanceDeletion: true, plans } } function uniqueNames(values: string[]): string[] { return Array.from(new Set(values)) } function buildInstanceSpecs(packages: SeedPackageSpec[]): SeedInstanceSpec[] { return Array.from({ length: 64 }, (_, index) => { const id = index + 1 const packageSpec = packages[index % packages.length] const planName = packageSpec.plans[index % packageSpec.plans.length].name const hostName = packageSpec.hostNames[index % packageSpec.hostNames.length] const status = pickInstanceStatus(index) const createdAt = daysAgo(4 + (index % 120), 8 + (index % 6)) const expiresAt = status === 'suspended' ? daysAgo(1 + (index % 4), 9) : daysFromNow(20 + (index % 90), 9) const ipv4 = ['nat', 'nat_ipv6', 'nat_ipv6_nat'].includes(packageSpec.networkMode) ? `10.${60 + (index % 20)}.${1 + Math.floor(index / 20)}.${20 + (index % 180)}` : null const routedIpv6 = packageSpec.networkMode === 'nat_ipv6' || packageSpec.networkMode === 'ipv6_only' ? `2001:db8:${(0x900 + index).toString(16)}::${100 + id}` : null const sharedIpv6 = packageSpec.networkMode === 'nat_ipv6_nat' || packageSpec.networkMode === 'ipv6_nat' ? `2001:db8:share::${id}` : null const primaryIpv6 = routedIpv6 ?? sharedIpv6 const firstPublicPort = packageSpec.networkMode === 'ipv6_only' ? null : 21000 + index * 6 const trafficStatus: SeedTrafficStatus = index % 11 === 0 ? 'LIMITED' : index % 7 === 0 ? 'WARNING' : 'NORMAL' const monthlyTrafficUsed = trafficStatus === 'LIMITED' ? bytesFromGb(240 + index * 3) : trafficStatus === 'WARNING' ? bytesFromGb(180 + index * 2) : bytesFromGb(70 + index * 2) return { name: `${SEED_PREFIX}-inst-${pad(id, 3)}`, incusId: `${SEED_PREFIX}-incus-${pad(id, 3)}`, userUsername: customerUsers[(index * 7) % customerUsers.length].username, packageKey: packageSpec.key, planName, hostName, image: packageSpec.instanceType === 'vm' ? vmImages[index % vmImages.length] : containerImages[index % containerImages.length], status, createdAt, expiresAt, suspendedAt: status === 'suspended' ? daysAgo(index % 4, 9) : null, suspendedBy: status === 'suspended' && index % 2 === 0 ? 1 : null, suspendReason: status === 'suspended' ? `${SEED_MARK} expired sample` : null, autoRenew: index % 3 !== 0, cloudInitState: status === 'error' ? 'error' : 'done', cloudInitSource: packageSpec.instanceType === 'vm' ? 'cloud-init' : 'lxd-agent', ipv4, ipv6: primaryIpv6, sshPort: firstPublicPort ?? 22, monthlyTrafficUsed, trafficStatus, lastIpv6ReassignAt: packageSpec.networkMode === 'nat_ipv6' && index % 5 === 0 ? daysAgo(index % 3, 7) : null, lastPlanChangeAt: index % 6 === 0 ? daysAgo(2 + (index % 9), 11) : null, lastSyncedAt: daysAgo(index % 5, 14) } }) } function pickInstanceStatus(index: number): SeedInstanceStatus { const mod = index % 20 if (mod === 17) return 'error' if (mod === 18) return 'suspended' if (mod === 19 || mod === 14 || mod === 15) return 'stopped' return 'running' } async function ensureLocalDevGuard(): Promise { const databaseUrl = process.env.DATABASE_URL || '' const isDevDb = databaseUrl.includes('incudal_dev_password@db:5432/incudal') if (!isDevDb && process.env.FORCE_BULK_SEED !== '1') { throw new Error( `Refusing to run ${SEED_PREFIX} seed outside the local dev database. Set FORCE_BULK_SEED=1 to override.` ) } } async function buildPasswordHash(password: string): Promise { return bcrypt.hash(password, LOCAL_PASSWORD_SALT) } async function syncUsers(passwordHash: string): Promise> { const userIds = new Map() for (const [index, user] of allSeedUsers.entries()) { const saved = await prisma.user.upsert({ where: { username: user.username }, create: { username: user.username, email: user.email, passwordHash, role: user.role, status: user.status, banReason: user.banReason, balance: 0, avatarStyle: user.avatarStyle }, update: { email: user.email, passwordHash, role: user.role, status: user.status, banReason: user.banReason, avatarStyle: user.avatarStyle } }) await prisma.userQuota.upsert({ where: { userId: saved.id }, create: { userId: saved.id, hostLimit: user.quota.hostLimit, packageLimit: user.quota.packageLimit, monthlyTrafficLimit: user.quota.monthlyTrafficLimit }, update: { hostLimit: user.quota.hostLimit, packageLimit: user.quota.packageLimit, monthlyTrafficLimit: user.quota.monthlyTrafficLimit } }) userIds.set(user.username, saved.id) } const seedUserIds = Array.from(userIds.values()) await prisma.loginRecord.deleteMany({ where: { userId: { in: seedUserIds }, userAgent: { startsWith: `${SEED_PREFIX}/` } } }) for (const [index, user] of allSeedUsers.entries()) { const userId = userIds.get(user.username)! const firstCountry = countries[index % countries.length] const secondCountry = countries[(index + 2) % countries.length] await prisma.loginRecord.createMany({ data: [ { userId, ip: `172.19.${10 + (index % 20)}.${20 + (index % 180)}`, country: firstCountry.toUpperCase(), region: cities[index % cities.length], city: cities[index % cities.length], isp: 'BulkSeedNet', timezone: 'Asia/Shanghai', userAgent: `${SEED_PREFIX}/web-chrome` }, { userId, ip: `172.20.${10 + (index % 20)}.${40 + (index % 180)}`, country: secondCountry.toUpperCase(), region: cities[(index + 1) % cities.length], city: cities[(index + 1) % cities.length], isp: 'BulkSeedMobile', timezone: 'Asia/Shanghai', userAgent: `${SEED_PREFIX}/mobile-safari` } ] }) } return userIds } async function syncHosts(userIds: Map): Promise> { const hostIds = new Map() for (const host of hostSpecs) { const userId = userIds.get(host.ownerUsername) if (!userId) { throw new Error(`Missing owner for host ${host.name}`) } const saved = await prisma.host.upsert({ where: { userId_name: { userId, name: host.name } }, create: { userId, name: host.name, url: host.url, location: host.location, countryCode: host.countryCode, architecture: host.architecture, status: host.status, natPublicIp: host.natPublicIp, natPublicIpv6: host.natPublicIpv6, natBindIp: host.natBindIp, natBindIpv6: host.natBindIpv6, natPortStart: host.natPortStart, natPortEnd: host.natPortEnd, tags: host.tags, cpuAllowanceMax: host.cpuAllowanceMax, memoryMax: host.memoryMax, instanceType: host.instanceType, ipAddress: host.ipAddress, storageDriver: 'zfs', storageType: 'loop', storageSize: host.storageSize, ipv6Mode: 1, ipv6Subnet: host.ipv6Subnet, ipv6Gateway: host.ipv6Gateway, ipv6ParentInterface: 'eth0', enableApi: true, isInstalled: true, caddyEnabled: host.caddyEnabled, caddyUsername: host.caddyEnabled ? `${SEED_PREFIX}-caddy` : null, caddyPassword: host.caddyEnabled ? `${SEED_PREFIX}-password` : null, caddyPort: 8444 + (host.natPortStart % 10), transferEnabled: true, trafficResetDay: 1 + (host.natPortStart % 20), announcement: `${SEED_MARK} ${host.location} demo host`, notifyPurchase: true, notifyRenew: true, notifyDestroy: host.instanceType === 'vm', enableResourcePool: true, probeUrl: `https://${host.name}.probe.local/health` }, update: { url: host.url, location: host.location, countryCode: host.countryCode, architecture: host.architecture, status: host.status, natPublicIp: host.natPublicIp, natPublicIpv6: host.natPublicIpv6, natBindIp: host.natBindIp, natBindIpv6: host.natBindIpv6, natPortStart: host.natPortStart, natPortEnd: host.natPortEnd, tags: host.tags, cpuAllowanceMax: host.cpuAllowanceMax, memoryMax: host.memoryMax, instanceType: host.instanceType, ipAddress: host.ipAddress, storageDriver: 'zfs', storageType: 'loop', storageSize: host.storageSize, ipv6Mode: 1, ipv6Subnet: host.ipv6Subnet, ipv6Gateway: host.ipv6Gateway, ipv6ParentInterface: 'eth0', enableApi: true, isInstalled: true, caddyEnabled: host.caddyEnabled, caddyUsername: host.caddyEnabled ? `${SEED_PREFIX}-caddy` : null, caddyPassword: host.caddyEnabled ? `${SEED_PREFIX}-password` : null, transferEnabled: true, announcement: `${SEED_MARK} ${host.location} demo host`, probeUrl: `https://${host.name}.probe.local/health` } }) hostIds.set(host.name, saved.id) for (const pool of storagePools) { await prisma.storagePool.upsert({ where: { hostId_name: { hostId: saved.id, name: pool.name } }, create: { hostId: saved.id, name: pool.name, driver: pool.driver, purpose: pool.purpose, description: pool.description, config: { generatedBy: SEED_PREFIX, host: host.name } }, update: { driver: pool.driver, purpose: pool.purpose, description: pool.description, config: { generatedBy: SEED_PREFIX, host: host.name } } }) } } return hostIds } async function syncPackages( userIds: Map, hostIds: Map ): Promise> { const packageMap = new Map() for (const pkg of packageSpecs) { const userId = userIds.get(pkg.ownerUsername) if (!userId) { throw new Error(`Missing package owner ${pkg.ownerUsername}`) } const saved = await prisma.package.upsert({ where: { userId_name: { userId, name: pkg.name } }, create: { userId, name: pkg.name, description: pkg.description, cpuMax: pkg.cpuMax, memoryMax: pkg.memoryMax, diskMax: pkg.diskMax, bandwidthMax: pkg.bandwidthMax, networkMode: pkg.networkMode, instanceType: pkg.instanceType, privileged: pkg.privileged, nested: pkg.nested, active: pkg.active, portLimit: pkg.portLimit, snapshotLimit: pkg.snapshotLimit, backupLimit: pkg.backupLimit, siteLimit: pkg.siteLimit, monthlyTrafficLimit: pkg.monthlyTrafficLimit, ioLimitMode: pkg.ioLimitMode, limitsRead: pkg.limitsRead, limitsWrite: pkg.limitsWrite, limitsReadIops: pkg.limitsReadIops, limitsWriteIops: pkg.limitsWriteIops, limitsIngress: pkg.limitsIngress, limitsEgress: pkg.limitsEgress, limitsProcesses: pkg.limitsProcesses, limitsCpuPriority: pkg.limitsCpuPriority, bootAutostart: pkg.bootAutostart, bootAutostartPriority: pkg.bootAutostartPriority, bootAutostartDelay: pkg.bootAutostartDelay, bootHostShutdownTimeout: pkg.bootHostShutdownTimeout, globalShared: pkg.globalShared, globalQuotaMultiplier: pkg.globalQuotaMultiplier, globalMaxInstances: pkg.globalMaxInstances, allowInstanceDeletion: pkg.allowInstanceDeletion }, update: { description: pkg.description, cpuMax: pkg.cpuMax, memoryMax: pkg.memoryMax, diskMax: pkg.diskMax, bandwidthMax: pkg.bandwidthMax, networkMode: pkg.networkMode, instanceType: pkg.instanceType, privileged: pkg.privileged, nested: pkg.nested, active: pkg.active, portLimit: pkg.portLimit, snapshotLimit: pkg.snapshotLimit, backupLimit: pkg.backupLimit, siteLimit: pkg.siteLimit, monthlyTrafficLimit: pkg.monthlyTrafficLimit, ioLimitMode: pkg.ioLimitMode, limitsRead: pkg.limitsRead, limitsWrite: pkg.limitsWrite, limitsReadIops: pkg.limitsReadIops, limitsWriteIops: pkg.limitsWriteIops, limitsIngress: pkg.limitsIngress, limitsEgress: pkg.limitsEgress, limitsProcesses: pkg.limitsProcesses, limitsCpuPriority: pkg.limitsCpuPriority, bootAutostart: pkg.bootAutostart, bootAutostartPriority: pkg.bootAutostartPriority, bootAutostartDelay: pkg.bootAutostartDelay, bootHostShutdownTimeout: pkg.bootHostShutdownTimeout, globalShared: pkg.globalShared, globalQuotaMultiplier: pkg.globalQuotaMultiplier, globalMaxInstances: pkg.globalMaxInstances, allowInstanceDeletion: pkg.allowInstanceDeletion } }) await prisma.packageHost.deleteMany({ where: { packageId: saved.id } }) await prisma.packageHost.createMany({ data: pkg.hostNames.map(hostName => { const hostId = hostIds.get(hostName) if (!hostId) { throw new Error(`Missing host ${hostName} for package ${pkg.name}`) } return { packageId: saved.id, hostId, storagePoolName: 'default' } }) }) for (const plan of pkg.plans) { await prisma.packagePlan.upsert({ where: { packageId_name: { packageId: saved.id, name: plan.name } }, create: { packageId: saved.id, name: plan.name, description: plan.description, cpu: plan.cpu, memory: plan.memory, disk: plan.disk, portLimit: plan.portLimit, snapshotLimit: plan.snapshotLimit, backupLimit: plan.backupLimit, siteLimit: plan.siteLimit, swapSize: plan.swapSize, trafficLimit: plan.trafficLimit, trafficLimitSpeed: plan.trafficLimitSpeed, price: plan.price, billingCycle: plan.billingCycle, setupFee: plan.setupFee, isActive: plan.isActive, sortOrder: plan.sortOrder, slaGuarantee: plan.slaGuarantee }, update: { description: plan.description, cpu: plan.cpu, memory: plan.memory, disk: plan.disk, portLimit: plan.portLimit, snapshotLimit: plan.snapshotLimit, backupLimit: plan.backupLimit, siteLimit: plan.siteLimit, swapSize: plan.swapSize, trafficLimit: plan.trafficLimit, trafficLimitSpeed: plan.trafficLimitSpeed, price: plan.price, billingCycle: plan.billingCycle, setupFee: plan.setupFee, isActive: plan.isActive, sortOrder: plan.sortOrder, slaGuarantee: plan.slaGuarantee } }) } const savedPlans = await prisma.packagePlan.findMany({ where: { packageId: saved.id }, orderBy: { sortOrder: 'asc' } }) packageMap.set(pkg.key, { id: saved.id, spec: pkg, planIdsByName: new Map(savedPlans.map(plan => [plan.name, plan.id])) }) } return packageMap } async function syncInstances( userIds: Map, hostIds: Map, packagesByKey: Map ): Promise { const records: GeneratedInstanceRecord[] = [] for (const [index, instance] of instanceSpecs.entries()) { const userId = userIds.get(instance.userUsername) const pkg = packagesByKey.get(instance.packageKey) const hostId = hostIds.get(instance.hostName) const hostSpec = hostSpecs.find(candidate => candidate.name === instance.hostName) if (!userId || !pkg || !hostId || !hostSpec) { throw new Error(`Missing relation while seeding instance ${instance.name}`) } const planId = pkg.planIdsByName.get(instance.planName) const plan = pkg.spec.plans.find(candidate => candidate.name === instance.planName) if (!planId || !plan) { throw new Error(`Missing plan ${instance.planName} for ${instance.name}`) } const existing = await prisma.instance.findFirst({ where: { userId, name: instance.name } }) const saved = existing ? await prisma.instance.update({ where: { id: existing.id }, data: buildInstanceData(instance, userId, hostId, hostSpec, pkg.id, planId, pkg.spec, plan, index) }) : await prisma.instance.create({ data: buildInstanceData(instance, userId, hostId, hostSpec, pkg.id, planId, pkg.spec, plan, index) }) await syncInstanceChildren(saved.id, userId, hostId, hostSpec, instance, pkg.spec, plan, index) records.push({ id: saved.id, userId, hostId, packageId: pkg.id, packagePlanId: planId, spec: instance, packageSpec: pkg.spec, plan }) } return records } function buildInstanceData( spec: SeedInstanceSpec, userId: number, hostId: number, hostSpec: SeedHostSpec, packageId: number, packagePlanId: number, pkg: SeedPackageSpec, plan: SeedPlanSpec, index: number ) { return { incusId: spec.incusId, name: spec.name, userId, hostId, packageId, packagePlanId, storagePoolName: 'default', image: spec.image, status: spec.status, cpu: plan.cpu, memory: plan.memory, disk: plan.disk, ipv4: spec.ipv4, ipv6: spec.ipv6, networkMode: pkg.networkMode, sshPort: pkg.networkMode === 'ipv6_only' ? 22 : hostSpec.natPortStart + 20 + index * 4, rootPassword: `${SHARED_PASSWORD}!${pad(index + 1, 3)}`, snapshottedSpecs: { generatedBy: SEED_PREFIX, packageKey: pkg.key, planName: plan.name, cpu: plan.cpu, memory: plan.memory, disk: plan.disk }, portLimit: plan.portLimit, snapshotLimit: plan.snapshotLimit, backupLimit: plan.backupLimit, siteLimit: plan.siteLimit, swapEnabled: plan.swapSize > 0, swapSize: plan.swapSize, monthlyTrafficLimit: plan.trafficLimit, monthlyTrafficUsed: spec.monthlyTrafficUsed, trafficStatus: spec.trafficStatus, limitsRead: pkg.limitsRead, limitsWrite: pkg.limitsWrite, limitsReadIops: pkg.limitsReadIops, limitsWriteIops: pkg.limitsWriteIops, limitsIngress: pkg.limitsIngress, limitsEgress: pkg.limitsEgress, limitsProcesses: pkg.limitsProcesses, limitsCpuPriority: pkg.limitsCpuPriority, bootAutostart: pkg.bootAutostart, bootAutostartPriority: pkg.bootAutostartPriority, bootAutostartDelay: pkg.bootAutostartDelay, bootHostShutdownTimeout: pkg.bootHostShutdownTimeout, expiresAt: spec.expiresAt, suspendedAt: spec.suspendedAt, suspendedBy: spec.suspendedBy, suspendReason: spec.suspendReason, billingPrice: plan.price, billingCycle: plan.billingCycle, autoRenew: spec.autoRenew, iconBadgeId: index % 9 === 0 ? 'fire' : null, autoRenewAttempts: spec.autoRenew ? index % 3 : 0, lastAutoRenewAttemptAt: spec.autoRenew ? daysAgo(index % 5, 16) : null, expiryNotifiedAt: spec.status === 'suspended' ? daysAgo(1, 17) : null, version: index % 5, lastIpv6ReassignAt: spec.lastIpv6ReassignAt, lastPlanChangeAt: spec.lastPlanChangeAt, cloudInitState: spec.cloudInitState, cloudInitSource: spec.cloudInitSource, cloudInitLastCheckedAt: spec.lastSyncedAt, cloudInitCompletedAt: spec.status === 'error' ? null : addMonths(spec.createdAt, 0), cloudInitManualCompletedAt: spec.status === 'error' ? null : null, cloudInitManualCompletedBy: null, createdAt: spec.createdAt, lastSyncedAt: spec.lastSyncedAt } } async function syncInstanceChildren( instanceId: number, userId: number, hostId: number, hostSpec: SeedHostSpec, spec: SeedInstanceSpec, pkg: SeedPackageSpec, plan: SeedPlanSpec, index: number ): Promise { await prisma.portMapping.deleteMany({ where: { instanceId } }) await prisma.snapshot.deleteMany({ where: { instanceId } }) await prisma.backup.deleteMany({ where: { instanceId } }) await prisma.proxySite.deleteMany({ where: { instanceId } }) await prisma.ipAddress.deleteMany({ where: { instanceId } }) await prisma.ipv6Subnet.deleteMany({ where: { instanceId } }) await prisma.dailyTraffic.deleteMany({ where: { instanceId } }) await prisma.instanceBillingRecord.deleteMany({ where: { instanceId } }) const publicBase = hostSpec.natPortStart + 20 + index * 4 const supportsPortMappings = pkg.networkMode !== 'ipv6_only' && plan.portLimit > 0 if (supportsPortMappings) { const portSet = pickPortSet(index, pkg.instanceType) const mappings = portSet.map((privatePort, mappingIndex) => ({ instanceId, hostId, protocol: pickProtocol(index, mappingIndex), publicPort: publicBase + mappingIndex, privatePort, remark: `${SEED_MARK} ${mappingIndex === 0 ? 'ssh-or-app' : 'service'}` })) await prisma.portMapping.createMany({ data: mappings }) } const snapshotsToCreate = index % 3 === 0 ? 2 : index % 2 === 0 ? 1 : 0 if (snapshotsToCreate > 0) { await prisma.snapshot.createMany({ data: Array.from({ length: snapshotsToCreate }, (_, snapshotIndex) => ({ instanceId, incusName: `${spec.name}/snap-${snapshotIndex + 1}`, name: `${SEED_PREFIX}-snap-${snapshotIndex + 1}`, description: `${SEED_MARK} snapshot ${snapshotIndex + 1}`, stateful: pkg.instanceType === 'vm' && snapshotIndex === 0, size: 1024 + snapshotIndex * 512 })) }) } const backupsToCreate = index % 4 === 0 ? 2 : index % 3 === 0 ? 1 : 0 if (backupsToCreate > 0) { await prisma.backup.createMany({ data: Array.from({ length: backupsToCreate }, (_, backupIndex) => ({ instanceId, incusName: `${spec.name}/backup-${backupIndex + 1}`, name: `${SEED_PREFIX}-backup-${backupIndex + 1}`, description: `${SEED_MARK} backup ${backupIndex + 1}`, size: 2048 + backupIndex * 768, status: backupIndex === backupsToCreate - 1 && index % 8 === 0 ? 'error' : ('ready' as SeedBackupStatus), expiresAt: daysFromNow(14 + backupIndex * 7, 6) })) }) } if (index % 4 === 0) { await prisma.backupPolicy.upsert({ where: { instanceId }, create: { instanceId, enabled: true, intervalMinutes: 720, lastRunAt: daysAgo(1, 2), nextRunAt: daysFromNow(1, 2) }, update: { enabled: true, intervalMinutes: 720, lastRunAt: daysAgo(1, 2), nextRunAt: daysFromNow(1, 2) } }) } else { await prisma.backupPolicy.deleteMany({ where: { instanceId } }) } if (index % 3 === 0) { await prisma.snapshotPolicy.upsert({ where: { instanceId }, create: { instanceId, enabled: true, intervalMinutes: 360, lastRunAt: daysAgo(1, 4), nextRunAt: daysFromNow(1, 4) }, update: { enabled: true, intervalMinutes: 360, lastRunAt: daysAgo(1, 4), nextRunAt: daysFromNow(1, 4) } }) } else { await prisma.snapshotPolicy.deleteMany({ where: { instanceId } }) } await prisma.trafficSnapshot.upsert({ where: { instanceId }, create: { instanceId, rxRaw: 100_000_000n + BigInt(index) * 10_000n, txRaw: 80_000_000n + BigInt(index) * 9_000n }, update: { rxRaw: 100_000_000n + BigInt(index) * 10_000n, txRaw: 80_000_000n + BigInt(index) * 9_000n } }) await prisma.dailyTraffic.createMany({ data: Array.from({ length: 7 }, (_, dayOffset) => ({ instanceId, date: daysAgo(6 - dayOffset, 0), rxTotal: 2_000_000_000n + BigInt(index + dayOffset) * 30_000_000n, txTotal: 1_200_000_000n + BigInt(index + dayOffset) * 20_000_000n })) }) const shouldAddIpv6 = pkg.networkMode === 'nat_ipv6' || pkg.networkMode === 'ipv6_only' if (shouldAddIpv6 && spec.ipv6) { await prisma.ipAddress.createMany({ data: [ { instanceId, address: spec.ipv6, type: 'inet6', isPrimary: true, isCustom: false, device: 'eth1' }, ...(index % 5 === 0 ? [ { instanceId, address: `${spec.ipv6.split('::')[0]}::${400 + index}`, type: 'inet6', isPrimary: false, isCustom: true, device: 'eth1' } ] : []) ] }) } if (shouldAddIpv6 && spec.ipv6 && index % 6 === 0) { await prisma.ipv6Subnet.create({ data: { instanceId, cidr: `${spec.ipv6.split('::')[0]}:100::/120`, primaryIp: `${spec.ipv6.split('::')[0]}:100::1`, device: 'eth1' } }) } const proxyCount = pkg.instanceType === 'container' && plan.siteLimit > 0 && index % 3 !== 0 ? (index % 5 === 0 ? 2 : 1) : 0 if (proxyCount > 0) { await prisma.proxySite.createMany({ data: Array.from({ length: proxyCount }, (_, siteIndex) => ({ instanceId, hostId, domain: `${spec.name}-${siteIndex + 1}.${SEED_PREFIX}.test`, targetPort: [80, 443, 8080][siteIndex % 3], httpsEnabled: siteIndex % 2 === 0, remark: `${SEED_MARK} proxy ${siteIndex + 1}`, status: pickProxyStatus(index, siteIndex), enabled: siteIndex % 4 !== 0, error: index % 9 === 0 && siteIndex === 0 ? `${SEED_MARK} DNS pending` : null })) }) } await prisma.instanceBillingRecord.create({ data: { instanceId, userId, type: 'newPurchase', amount: plan.price, months: plan.billingCycle, periodStart: spec.createdAt, periodEnd: addMonths(spec.createdAt, plan.billingCycle), remark: `${SEED_MARK} initial purchase` } }) if (index % 6 === 0) { const renewedStart = addMonths(spec.createdAt, plan.billingCycle) await prisma.instanceBillingRecord.create({ data: { instanceId, userId, type: 'renew', amount: plan.price, months: plan.billingCycle, periodStart: renewedStart, periodEnd: addMonths(renewedStart, plan.billingCycle), remark: `${SEED_MARK} renewal sample` } }) } } function pickPortSet(index: number, instanceType: SeedPackageInstanceType): number[] { if (instanceType === 'vm') { return index % 2 === 0 ? [22, 3389] : [22, 8443] } const sets = [ [22, 80], [22, 443], [25565, 19132], [8080, 8443] ] return sets[index % sets.length] } function pickProtocol(index: number, mappingIndex: number): SeedProtocol { if (mappingIndex === 1 && index % 4 === 2) return 'udp' return 'tcp' } function pickProxyStatus(index: number, siteIndex: number): SeedProxyStatus { if (index % 9 === 0 && siteIndex === 0) return 'error' if (index % 4 === 0) return 'pending' return 'active' } async function syncTickets(instances: GeneratedInstanceRecord[]): Promise { await prisma.ticket.deleteMany({ where: { subject: { startsWith: SEED_MARK } } }) const selected = instances.filter((_, index) => index % 5 === 0).slice(0, 12) for (const [index, instance] of selected.entries()) { const hostOwner = await prisma.host.findUniqueOrThrow({ where: { id: instance.hostId }, select: { userId: true } }) const status = (['open', 'in_progress', 'resolved', 'closed'] as SeedTicketStatus[])[index % 4] const createdAt = daysAgo(2 + index, 9) await prisma.ticket.create({ data: { userId: instance.userId, hostId: instance.hostId, instanceId: instance.id, subject: `${SEED_MARK} ticket-${pad(index + 1, 2)} for ${instance.spec.name}`, category: ['general', 'billing', 'technical', 'abuse'][index % 4], priority: (['low', 'normal', 'high', 'urgent'] as SeedTicketPriority[])[index % 4], status, createdAt, resolvedAt: status === 'resolved' || status === 'closed' ? daysAgo(1 + index, 12) : null, closedAt: status === 'closed' ? daysAgo(index, 15) : null, messages: { create: [ { senderId: instance.userId, content: `${SEED_MARK} user asks about ${instance.spec.name}`, isFromOwner: false, createdAt }, { senderId: hostOwner.userId, content: `${SEED_MARK} host owner replied for ${instance.spec.name}`, isFromOwner: true, createdAt: new Date(createdAt.getTime() + 60 * 60 * 1000) }, ...(index % 3 === 0 ? [ { senderId: instance.userId, content: `${SEED_MARK} follow-up confirmation`, isFromOwner: false, createdAt: new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) } ] : []) ] } } }) } } async function syncBalances( userIds: Map, instances: GeneratedInstanceRecord[] ): Promise { const seedUserIds = Array.from(userIds.values()) await prisma.balanceLog.deleteMany({ where: { userId: { in: seedUserIds }, remark: { startsWith: SEED_MARK } } }) const spendByUser = new Map() for (const instance of instances) { spendByUser.set(instance.userId, (spendByUser.get(instance.userId) || 0) + instance.plan.price) } const logs: SeedBalanceLogSpec[] = [] for (const [index, user] of allSeedUsers.entries()) { const userId = userIds.get(user.username)! const spend = spendByUser.get(userId) || 0 const baseRecharge = getBaseRechargeAmount(user, index, spend) const giftAmount = getGiftAmount(user, index) let balanceBefore = 0 logs.push({ userId, type: user.role === 'admin' ? 'admin_adjust' : 'recharge', amount: baseRecharge, balanceBefore, balanceAfter: balanceBefore + baseRecharge, orderId: user.role === 'admin' ? null : getSeedOrderNo(index), remark: `${SEED_MARK} initial funding` }) balanceBefore += baseRecharge logs.push({ userId, type: 'gift', amount: giftAmount, balanceBefore, balanceAfter: balanceBefore + giftAmount, orderId: null, remark: `${SEED_MARK} promo gift` }) balanceBefore += giftAmount if (spend > 0) { logs.push({ userId, type: 'consume', amount: -spend, balanceBefore, balanceAfter: balanceBefore - spend, orderId: null, remark: `${SEED_MARK} instance purchases` }) balanceBefore -= spend } await prisma.user.update({ where: { id: userId }, data: { balance: balanceBefore } }) } await prisma.balanceLog.createMany({ data: logs }) } async function refreshHostAndQuotaUsage( userIds: Map, hostIds: Map, instances: GeneratedInstanceRecord[] ): Promise { const hostStats = new Map() for (const hostId of hostIds.values()) { hostStats.set(hostId, { cpu: 0, memory: 0, disk: 0, ports: 0 }) } for (const instance of instances) { const stats = hostStats.get(instance.hostId) if (!stats) continue stats.cpu += instance.plan.cpu stats.memory += instance.plan.memory stats.disk += instance.plan.disk } const portCounts = await prisma.portMapping.groupBy({ by: ['hostId'], where: { hostId: { in: Array.from(hostIds.values()) } }, _count: { _all: true } }) for (const row of portCounts) { const stats = hostStats.get(row.hostId) if (stats) { stats.ports = row._count._all } } for (const [hostName, hostId] of hostIds.entries()) { const stats = hostStats.get(hostId)! await prisma.host.update({ where: { id: hostId }, data: { cpuUsed: stats.cpu, memoryUsed: stats.memory, diskUsed: stats.disk, natPortsUsedCount: stats.ports } }) } const hostCountByOwner = new Map() const packageCountByOwner = new Map() ownerUsers.forEach(owner => { const ownerId = userIds.get(owner.username)! hostCountByOwner.set(ownerId, hostSpecs.filter(host => host.ownerUsername === owner.username).length) packageCountByOwner.set(ownerId, packageSpecs.filter(pkg => pkg.ownerUsername === owner.username).length) }) for (const user of allSeedUsers) { const userId = userIds.get(user.username)! await prisma.userQuota.update({ where: { userId }, data: { hostUsed: hostCountByOwner.get(userId) || 0, packageUsed: packageCountByOwner.get(userId) || 0 } }) } } async function printSummary(): Promise { const [users, hosts, packages, plans, instances, ports, snapshots, backups, sites, tickets] = await Promise.all([ prisma.user.count({ where: { username: { startsWith: SEED_PREFIX } } }), prisma.host.count({ where: { name: { startsWith: `${SEED_PREFIX}-host-` } } }), prisma.package.count({ where: { name: { startsWith: `${SEED_PREFIX}-pkg-` } } }), prisma.packagePlan.count({ where: { package: { name: { startsWith: `${SEED_PREFIX}-pkg-` } } } }), prisma.instance.count({ where: { name: { startsWith: `${SEED_PREFIX}-inst-` } } }), prisma.portMapping.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }), prisma.snapshot.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }), prisma.backup.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }), prisma.proxySite.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }), prisma.ticket.count({ where: { subject: { startsWith: SEED_MARK } } }) ]) console.log(`${SEED_MARK} completed`) console.log(`${SEED_MARK} users=${users}, hosts=${hosts}, packages=${packages}, plans=${plans}, instances=${instances}`) console.log(`${SEED_MARK} portMappings=${ports}, snapshots=${snapshots}, backups=${backups}, proxySites=${sites}, tickets=${tickets}`) console.log(`${SEED_MARK} shared password for generated users: ${SHARED_PASSWORD}`) } async function main(): Promise { await ensureLocalDevGuard() const passwordHash = await buildPasswordHash(SHARED_PASSWORD) const userIds = await syncUsers(passwordHash) const hostIds = await syncHosts(userIds) const packagesByKey = await syncPackages(userIds, hostIds) const instances = await syncInstances(userIds, hostIds, packagesByKey) await syncTickets(instances) await syncBalances(userIds, instances) await refreshHostAndQuotaUsage(userIds, hostIds, instances) await printSummary() } void main() .catch(error => { console.error(`${SEED_MARK} failed:`, error) process.exitCode = 1 }) .finally(async () => { await prisma.$disconnect() })