feat: Complete rewrite as Next.js admin dashboard
Major transformation from FastAPI telemetry service to Next.js admin dashboard: - Next.js 15 App Router with TypeScript - Prisma ORM with PostgreSQL (same schema, new client) - TanStack Query for data fetching - Tailwind CSS + shadcn/ui components - Admin dashboard with: - Dashboard stats overview - Customer management (list, detail, edit) - Order management (list, create, detail, logs) - Server monitoring (grid view) - Subscription management Pages implemented: - /admin (dashboard) - /admin/customers (list + [id] detail) - /admin/orders (list + [id] detail with SSE logs) - /admin/servers (grid view) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
263
prisma/schema.prisma
Normal file
263
prisma/schema.prisma
Normal file
@@ -0,0 +1,263 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ENUMS
|
||||
// ============================================================================
|
||||
|
||||
enum UserStatus {
|
||||
PENDING_VERIFICATION
|
||||
ACTIVE
|
||||
SUSPENDED
|
||||
}
|
||||
|
||||
enum StaffRole {
|
||||
ADMIN
|
||||
SUPPORT
|
||||
}
|
||||
|
||||
enum SubscriptionPlan {
|
||||
TRIAL
|
||||
STARTER
|
||||
PRO
|
||||
ENTERPRISE
|
||||
}
|
||||
|
||||
enum SubscriptionTier {
|
||||
HUB_DASHBOARD
|
||||
ADVANCED
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
TRIAL
|
||||
ACTIVE
|
||||
CANCELED
|
||||
PAST_DUE
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PAYMENT_CONFIRMED
|
||||
AWAITING_SERVER
|
||||
SERVER_READY
|
||||
DNS_PENDING
|
||||
DNS_READY
|
||||
PROVISIONING
|
||||
FULFILLED
|
||||
EMAIL_CONFIGURED
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum JobStatus {
|
||||
PENDING
|
||||
CLAIMED
|
||||
RUNNING
|
||||
COMPLETED
|
||||
FAILED
|
||||
DEAD
|
||||
}
|
||||
|
||||
enum LogLevel {
|
||||
DEBUG
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USER & STAFF MODELS
|
||||
// ============================================================================
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
passwordHash String @map("password_hash")
|
||||
name String?
|
||||
company String?
|
||||
status UserStatus @default(PENDING_VERIFICATION)
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
subscriptions Subscription[]
|
||||
orders Order[]
|
||||
tokenUsage TokenUsage[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Staff {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
passwordHash String @map("password_hash")
|
||||
name String
|
||||
role StaffRole @default(SUPPORT)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("staff")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SUBSCRIPTION & BILLING
|
||||
// ============================================================================
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
plan SubscriptionPlan @default(TRIAL)
|
||||
tier SubscriptionTier @default(HUB_DASHBOARD)
|
||||
tokenLimit Int @default(10000) @map("token_limit")
|
||||
tokensUsed Int @default(0) @map("tokens_used")
|
||||
trialEndsAt DateTime? @map("trial_ends_at")
|
||||
stripeCustomerId String? @map("stripe_customer_id")
|
||||
stripeSubscriptionId String? @map("stripe_subscription_id")
|
||||
status SubscriptionStatus @default(TRIAL)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ORDERS & PROVISIONING
|
||||
// ============================================================================
|
||||
|
||||
model Order {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
status OrderStatus @default(PAYMENT_CONFIRMED)
|
||||
tier SubscriptionTier
|
||||
domain String
|
||||
tools String[]
|
||||
configJson Json @map("config_json")
|
||||
|
||||
// Server credentials (entered by staff)
|
||||
serverIp String? @map("server_ip")
|
||||
serverPasswordEncrypted String? @map("server_password_encrypted")
|
||||
sshPort Int @default(22) @map("ssh_port")
|
||||
|
||||
// Generated after provisioning
|
||||
portainerUrl String? @map("portainer_url")
|
||||
dashboardUrl String? @map("dashboard_url")
|
||||
failureReason String? @map("failure_reason")
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
serverReadyAt DateTime? @map("server_ready_at")
|
||||
provisioningStartedAt DateTime? @map("provisioning_started_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
provisioningLogs ProvisioningLog[]
|
||||
jobs ProvisioningJob[]
|
||||
|
||||
@@map("orders")
|
||||
}
|
||||
|
||||
model ProvisioningLog {
|
||||
id String @id @default(cuid())
|
||||
orderId String @map("order_id")
|
||||
level LogLevel @default(INFO)
|
||||
message String
|
||||
step String?
|
||||
timestamp DateTime @default(now())
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([orderId, timestamp])
|
||||
@@map("provisioning_logs")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JOB QUEUE
|
||||
// ============================================================================
|
||||
|
||||
model ProvisioningJob {
|
||||
id String @id @default(cuid())
|
||||
orderId String @map("order_id")
|
||||
jobType String @map("job_type")
|
||||
status JobStatus @default(PENDING)
|
||||
priority Int @default(0)
|
||||
claimedAt DateTime? @map("claimed_at")
|
||||
claimedBy String? @map("claimed_by")
|
||||
containerName String? @map("container_name")
|
||||
attempt Int @default(1)
|
||||
maxAttempts Int @default(3) @map("max_attempts")
|
||||
nextRetryAt DateTime? @map("next_retry_at")
|
||||
configSnapshot Json @map("config_snapshot")
|
||||
runnerTokenHash String? @map("runner_token_hash")
|
||||
result Json?
|
||||
error String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
logs JobLog[]
|
||||
|
||||
@@index([status, priority, createdAt])
|
||||
@@index([orderId])
|
||||
@@map("provisioning_jobs")
|
||||
}
|
||||
|
||||
model JobLog {
|
||||
id String @id @default(cuid())
|
||||
jobId String @map("job_id")
|
||||
level LogLevel @default(INFO)
|
||||
message String
|
||||
step String?
|
||||
progress Int?
|
||||
timestamp DateTime @default(now())
|
||||
|
||||
job ProvisioningJob @relation(fields: [jobId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([jobId, timestamp])
|
||||
@@map("job_logs")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOKEN USAGE (AI Tracking)
|
||||
// ============================================================================
|
||||
|
||||
model TokenUsage {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
instanceId String? @map("instance_id")
|
||||
operation String // chat, analysis, setup
|
||||
tokensInput Int @map("tokens_input")
|
||||
tokensOutput Int @map("tokens_output")
|
||||
model String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@map("token_usage")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RUNNER TOKENS
|
||||
// ============================================================================
|
||||
|
||||
model RunnerToken {
|
||||
id String @id @default(cuid())
|
||||
tokenHash String @unique @map("token_hash")
|
||||
name String
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
lastUsed DateTime? @map("last_used")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("runner_tokens")
|
||||
}
|
||||
320
prisma/seed.ts
Normal file
320
prisma/seed.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { PrismaClient, OrderStatus, SubscriptionPlan, SubscriptionTier, SubscriptionStatus, UserStatus, LogLevel } from '@prisma/client'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
const { hash } = bcrypt
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// Random data helpers
|
||||
const companies = [
|
||||
'Acme Corp', 'TechStart Inc', 'Digital Solutions', 'CloudFirst Ltd',
|
||||
'InnovateTech', 'DataDriven Co', 'SmartBiz Solutions', 'FutureTech Labs',
|
||||
'AgileWorks', 'NextGen Systems', null, null, null
|
||||
]
|
||||
|
||||
const domains = [
|
||||
'acme.letsbe.cloud', 'techstart.letsbe.cloud', 'digital.letsbe.cloud',
|
||||
'cloudfirst.letsbe.cloud', 'innovate.letsbe.cloud', 'datadriven.letsbe.cloud',
|
||||
'smartbiz.letsbe.cloud', 'futuretech.letsbe.cloud', 'agileworks.letsbe.cloud',
|
||||
'nextgen.letsbe.cloud', 'startup.letsbe.cloud', 'enterprise.letsbe.cloud',
|
||||
'demo.letsbe.cloud', 'test.letsbe.cloud', 'dev.letsbe.cloud'
|
||||
]
|
||||
|
||||
const toolSets = {
|
||||
basic: ['nextcloud', 'keycloak'],
|
||||
standard: ['nextcloud', 'keycloak', 'minio', 'poste'],
|
||||
advanced: ['nextcloud', 'keycloak', 'minio', 'poste', 'n8n', 'filebrowser'],
|
||||
full: ['nextcloud', 'keycloak', 'minio', 'poste', 'n8n', 'filebrowser', 'portainer', 'grafana'],
|
||||
}
|
||||
|
||||
const logMessages = {
|
||||
PAYMENT_CONFIRMED: ['Payment received via Stripe', 'Order confirmed'],
|
||||
AWAITING_SERVER: ['Waiting for server allocation', 'Server request submitted to provider'],
|
||||
SERVER_READY: ['Server provisioned', 'SSH access verified', 'Root password received'],
|
||||
DNS_PENDING: ['DNS records submitted', 'Waiting for DNS propagation'],
|
||||
DNS_READY: ['DNS records verified', 'Domain is resolving correctly'],
|
||||
PROVISIONING: [
|
||||
'Starting provisioning process',
|
||||
'Downloading Docker images',
|
||||
'Configuring Nginx reverse proxy',
|
||||
'Installing Keycloak',
|
||||
'Configuring Nextcloud',
|
||||
'Setting up MinIO storage',
|
||||
'Configuring email server',
|
||||
'Running health checks',
|
||||
],
|
||||
FULFILLED: ['Provisioning complete', 'All services healthy', 'Welcome email sent'],
|
||||
EMAIL_CONFIGURED: ['SMTP credentials configured', 'Email sending verified'],
|
||||
FAILED: ['Provisioning failed', 'See error details below'],
|
||||
}
|
||||
|
||||
function randomDate(daysAgo: number): Date {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * daysAgo))
|
||||
date.setHours(Math.floor(Math.random() * 24))
|
||||
date.setMinutes(Math.floor(Math.random() * 60))
|
||||
return date
|
||||
}
|
||||
|
||||
function randomChoice<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting seed...')
|
||||
|
||||
// 1. Create admin user if not exists
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@letsbe.solutions'
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'
|
||||
|
||||
const existingAdmin = await prisma.staff.findUnique({
|
||||
where: { email: adminEmail },
|
||||
})
|
||||
|
||||
if (!existingAdmin) {
|
||||
const passwordHash = await hash(adminPassword, 12)
|
||||
await prisma.staff.create({
|
||||
data: {
|
||||
email: adminEmail,
|
||||
passwordHash,
|
||||
name: 'Admin',
|
||||
role: 'ADMIN',
|
||||
},
|
||||
})
|
||||
console.log(`Created admin user: ${adminEmail}`)
|
||||
} else {
|
||||
console.log(`Admin user ${adminEmail} already exists`)
|
||||
}
|
||||
|
||||
// 2. Create support staff
|
||||
const supportEmail = 'support@letsbe.solutions'
|
||||
const existingSupport = await prisma.staff.findUnique({
|
||||
where: { email: supportEmail },
|
||||
})
|
||||
|
||||
if (!existingSupport) {
|
||||
const passwordHash = await hash('support123', 12)
|
||||
await prisma.staff.create({
|
||||
data: {
|
||||
email: supportEmail,
|
||||
passwordHash,
|
||||
name: 'Support Agent',
|
||||
role: 'SUPPORT',
|
||||
},
|
||||
})
|
||||
console.log(`Created support user: ${supportEmail}`)
|
||||
}
|
||||
|
||||
// 3. Create test customers
|
||||
const customerData = [
|
||||
{ email: 'john@acme.com', name: 'John Smith', company: 'Acme Corp', status: UserStatus.ACTIVE },
|
||||
{ email: 'sarah@techstart.io', name: 'Sarah Johnson', company: 'TechStart Inc', status: UserStatus.ACTIVE },
|
||||
{ email: 'mike@cloudfirst.co', name: 'Mike Davis', company: 'CloudFirst Ltd', status: UserStatus.ACTIVE },
|
||||
{ email: 'emma@digital.io', name: 'Emma Wilson', company: 'Digital Solutions', status: UserStatus.ACTIVE },
|
||||
{ email: 'david@innovate.co', name: 'David Brown', company: 'InnovateTech', status: UserStatus.ACTIVE },
|
||||
{ email: 'lisa@datadriven.io', name: 'Lisa Chen', company: 'DataDriven Co', status: UserStatus.ACTIVE },
|
||||
{ email: 'james@smartbiz.com', name: 'James Miller', company: 'SmartBiz Solutions', status: UserStatus.ACTIVE },
|
||||
{ email: 'amy@futuretech.io', name: 'Amy Taylor', company: 'FutureTech Labs', status: UserStatus.PENDING_VERIFICATION },
|
||||
{ email: 'robert@agile.co', name: 'Robert Anderson', company: 'AgileWorks', status: UserStatus.ACTIVE },
|
||||
{ email: 'jennifer@nextgen.io', name: 'Jennifer Lee', company: 'NextGen Systems', status: UserStatus.SUSPENDED },
|
||||
{ email: 'freelancer@gmail.com', name: 'Alex Freelancer', company: null, status: UserStatus.ACTIVE },
|
||||
{ email: 'startup@mail.com', name: 'Startup Founder', company: null, status: UserStatus.PENDING_VERIFICATION },
|
||||
]
|
||||
|
||||
const customers: { id: string; email: string }[] = []
|
||||
|
||||
for (const customer of customerData) {
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { email: customer.email },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
const passwordHash = await hash('customer123', 12)
|
||||
const created = await prisma.user.create({
|
||||
data: {
|
||||
email: customer.email,
|
||||
passwordHash,
|
||||
name: customer.name,
|
||||
company: customer.company,
|
||||
status: customer.status,
|
||||
emailVerified: customer.status === UserStatus.ACTIVE ? new Date() : null,
|
||||
},
|
||||
})
|
||||
customers.push({ id: created.id, email: created.email })
|
||||
console.log(`Created customer: ${customer.email}`)
|
||||
} else {
|
||||
customers.push({ id: existing.id, email: existing.email })
|
||||
console.log(`Customer ${customer.email} already exists`)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Create subscriptions for customers
|
||||
const subscriptionConfigs = [
|
||||
{ plan: SubscriptionPlan.ENTERPRISE, tier: SubscriptionTier.HUB_DASHBOARD, status: SubscriptionStatus.ACTIVE },
|
||||
{ plan: SubscriptionPlan.PRO, tier: SubscriptionTier.HUB_DASHBOARD, status: SubscriptionStatus.ACTIVE },
|
||||
{ plan: SubscriptionPlan.PRO, tier: SubscriptionTier.ADVANCED, status: SubscriptionStatus.ACTIVE },
|
||||
{ plan: SubscriptionPlan.STARTER, tier: SubscriptionTier.ADVANCED, status: SubscriptionStatus.ACTIVE },
|
||||
{ plan: SubscriptionPlan.STARTER, tier: SubscriptionTier.ADVANCED, status: SubscriptionStatus.ACTIVE },
|
||||
{ plan: SubscriptionPlan.PRO, tier: SubscriptionTier.HUB_DASHBOARD, status: SubscriptionStatus.PAST_DUE },
|
||||
{ plan: SubscriptionPlan.STARTER, tier: SubscriptionTier.ADVANCED, status: SubscriptionStatus.ACTIVE },
|
||||
{ plan: SubscriptionPlan.TRIAL, tier: SubscriptionTier.HUB_DASHBOARD, status: SubscriptionStatus.TRIAL },
|
||||
{ plan: SubscriptionPlan.ENTERPRISE, tier: SubscriptionTier.HUB_DASHBOARD, status: SubscriptionStatus.CANCELED },
|
||||
{ plan: SubscriptionPlan.STARTER, tier: SubscriptionTier.ADVANCED, status: SubscriptionStatus.ACTIVE },
|
||||
{ plan: SubscriptionPlan.TRIAL, tier: SubscriptionTier.ADVANCED, status: SubscriptionStatus.TRIAL },
|
||||
{ plan: SubscriptionPlan.TRIAL, tier: SubscriptionTier.HUB_DASHBOARD, status: SubscriptionStatus.TRIAL },
|
||||
]
|
||||
|
||||
for (let i = 0; i < customers.length; i++) {
|
||||
const customer = customers[i]
|
||||
const config = subscriptionConfigs[i] || subscriptionConfigs[0]
|
||||
|
||||
const existingSub = await prisma.subscription.findFirst({
|
||||
where: { userId: customer.id },
|
||||
})
|
||||
|
||||
if (!existingSub) {
|
||||
await prisma.subscription.create({
|
||||
data: {
|
||||
userId: customer.id,
|
||||
plan: config.plan,
|
||||
tier: config.tier,
|
||||
status: config.status,
|
||||
tokenLimit: config.plan === SubscriptionPlan.ENTERPRISE ? 100000 :
|
||||
config.plan === SubscriptionPlan.PRO ? 50000 :
|
||||
config.plan === SubscriptionPlan.STARTER ? 20000 : 10000,
|
||||
tokensUsed: Math.floor(Math.random() * 5000),
|
||||
trialEndsAt: config.status === SubscriptionStatus.TRIAL ?
|
||||
new Date(Date.now() + 14 * 24 * 60 * 60 * 1000) : null,
|
||||
},
|
||||
})
|
||||
console.log(`Created subscription for: ${customer.email}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Create orders with various statuses
|
||||
const orderConfigs = [
|
||||
// Orders in various pipeline stages
|
||||
{ status: OrderStatus.PAYMENT_CONFIRMED, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.PAYMENT_CONFIRMED, tier: SubscriptionTier.ADVANCED },
|
||||
{ status: OrderStatus.AWAITING_SERVER, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.AWAITING_SERVER, tier: SubscriptionTier.ADVANCED },
|
||||
{ status: OrderStatus.SERVER_READY, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.DNS_PENDING, tier: SubscriptionTier.ADVANCED },
|
||||
{ status: OrderStatus.DNS_PENDING, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.DNS_READY, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.DNS_READY, tier: SubscriptionTier.ADVANCED },
|
||||
{ status: OrderStatus.PROVISIONING, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.FULFILLED, tier: SubscriptionTier.ADVANCED },
|
||||
{ status: OrderStatus.FULFILLED, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.FULFILLED, tier: SubscriptionTier.ADVANCED },
|
||||
{ status: OrderStatus.EMAIL_CONFIGURED, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
{ status: OrderStatus.EMAIL_CONFIGURED, tier: SubscriptionTier.ADVANCED },
|
||||
{ status: OrderStatus.FAILED, tier: SubscriptionTier.HUB_DASHBOARD },
|
||||
]
|
||||
|
||||
for (let i = 0; i < orderConfigs.length; i++) {
|
||||
const config = orderConfigs[i]
|
||||
const customer = customers[i % customers.length]
|
||||
const domain = domains[i % domains.length]
|
||||
|
||||
const existingOrder = await prisma.order.findFirst({
|
||||
where: { domain },
|
||||
})
|
||||
|
||||
if (!existingOrder) {
|
||||
const tools = config.tier === SubscriptionTier.HUB_DASHBOARD
|
||||
? toolSets.full
|
||||
: randomChoice([toolSets.basic, toolSets.standard, toolSets.advanced])
|
||||
|
||||
const createdAt = randomDate(30)
|
||||
const serverStatuses: OrderStatus[] = [
|
||||
OrderStatus.SERVER_READY, OrderStatus.DNS_PENDING, OrderStatus.DNS_READY,
|
||||
OrderStatus.PROVISIONING, OrderStatus.FULFILLED, OrderStatus.EMAIL_CONFIGURED,
|
||||
OrderStatus.FAILED
|
||||
]
|
||||
const hasServer = serverStatuses.includes(config.status)
|
||||
|
||||
const order = await prisma.order.create({
|
||||
data: {
|
||||
userId: customer.id,
|
||||
status: config.status,
|
||||
tier: config.tier,
|
||||
domain,
|
||||
tools,
|
||||
configJson: { tools, tier: config.tier, domain },
|
||||
serverIp: hasServer ? `192.168.1.${100 + i}` : null,
|
||||
serverPasswordEncrypted: hasServer ? 'encrypted_placeholder' : null,
|
||||
sshPort: 22,
|
||||
portainerUrl: config.status === OrderStatus.FULFILLED || config.status === OrderStatus.EMAIL_CONFIGURED
|
||||
? `https://portainer.${domain}` : null,
|
||||
dashboardUrl: config.status === OrderStatus.FULFILLED || config.status === OrderStatus.EMAIL_CONFIGURED
|
||||
? `https://dashboard.${domain}` : null,
|
||||
failureReason: config.status === OrderStatus.FAILED
|
||||
? 'Connection timeout during Docker installation' : null,
|
||||
createdAt,
|
||||
serverReadyAt: hasServer ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : null,
|
||||
provisioningStartedAt: config.status === OrderStatus.PROVISIONING ||
|
||||
config.status === OrderStatus.FULFILLED || config.status === OrderStatus.EMAIL_CONFIGURED
|
||||
? new Date(createdAt.getTime() + 4 * 60 * 60 * 1000) : null,
|
||||
completedAt: config.status === OrderStatus.FULFILLED || config.status === OrderStatus.EMAIL_CONFIGURED
|
||||
? new Date(createdAt.getTime() + 5 * 60 * 60 * 1000) : null,
|
||||
},
|
||||
})
|
||||
|
||||
// Add provisioning logs based on status
|
||||
const statusIndex = Object.keys(logMessages).indexOf(config.status)
|
||||
const statusesToLog = Object.keys(logMessages).slice(0, statusIndex + 1) as OrderStatus[]
|
||||
|
||||
let logTime = new Date(createdAt)
|
||||
for (const logStatus of statusesToLog) {
|
||||
const messages = logMessages[logStatus] || []
|
||||
for (const message of messages) {
|
||||
await prisma.provisioningLog.create({
|
||||
data: {
|
||||
orderId: order.id,
|
||||
level: logStatus === OrderStatus.FAILED ? LogLevel.ERROR : LogLevel.INFO,
|
||||
message,
|
||||
step: logStatus,
|
||||
timestamp: new Date(logTime),
|
||||
},
|
||||
})
|
||||
logTime = new Date(logTime.getTime() + Math.random() * 5 * 60 * 1000) // 0-5 min later
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Created order: ${domain} (${config.status})`)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Create a runner token for testing
|
||||
const runnerTokenHash = await hash('test-runner-token', 12)
|
||||
const existingRunner = await prisma.runnerToken.findFirst({
|
||||
where: { name: 'test-runner' },
|
||||
})
|
||||
|
||||
if (!existingRunner) {
|
||||
await prisma.runnerToken.create({
|
||||
data: {
|
||||
tokenHash: runnerTokenHash,
|
||||
name: 'test-runner',
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
console.log('Created test runner token')
|
||||
}
|
||||
|
||||
console.log('\nSeed completed successfully!')
|
||||
console.log('\nTest credentials:')
|
||||
console.log(' Admin: admin@letsbe.solutions / admin123')
|
||||
console.log(' Support: support@letsbe.solutions / support123')
|
||||
console.log(' Customers: <email> / customer123')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user