Include full contents of all nested repositories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 16:25:02 +01:00
parent 14ff8fd54c
commit 2401ed446f
7271 changed files with 1310112 additions and 6 deletions

View File

@@ -0,0 +1,272 @@
-- CreateEnum
CREATE TYPE "UserStatus" AS ENUM ('PENDING_VERIFICATION', 'ACTIVE', 'SUSPENDED');
-- CreateEnum
CREATE TYPE "StaffRole" AS ENUM ('ADMIN', 'SUPPORT');
-- CreateEnum
CREATE TYPE "SubscriptionPlan" AS ENUM ('TRIAL', 'STARTER', 'PRO', 'ENTERPRISE');
-- CreateEnum
CREATE TYPE "SubscriptionTier" AS ENUM ('HUB_DASHBOARD', 'ADVANCED');
-- CreateEnum
CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIAL', 'ACTIVE', 'CANCELED', 'PAST_DUE');
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('PAYMENT_CONFIRMED', 'AWAITING_SERVER', 'SERVER_READY', 'DNS_PENDING', 'DNS_READY', 'PROVISIONING', 'FULFILLED', 'EMAIL_CONFIGURED', 'FAILED');
-- CreateEnum
CREATE TYPE "JobStatus" AS ENUM ('PENDING', 'CLAIMED', 'RUNNING', 'COMPLETED', 'FAILED', 'DEAD');
-- CreateEnum
CREATE TYPE "LogLevel" AS ENUM ('DEBUG', 'INFO', 'WARN', 'ERROR');
-- CreateEnum
CREATE TYPE "ServerConnectionStatus" AS ENUM ('PENDING', 'REGISTERED', 'ONLINE', 'OFFLINE');
-- CreateEnum
CREATE TYPE "CommandStatus" AS ENUM ('PENDING', 'SENT', 'EXECUTING', 'COMPLETED', 'FAILED');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"name" TEXT,
"company" TEXT,
"status" "UserStatus" NOT NULL DEFAULT 'PENDING_VERIFICATION',
"email_verified" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" "StaffRole" NOT NULL DEFAULT 'SUPPORT',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "staff_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "subscriptions" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"plan" "SubscriptionPlan" NOT NULL DEFAULT 'TRIAL',
"tier" "SubscriptionTier" NOT NULL DEFAULT 'HUB_DASHBOARD',
"token_limit" INTEGER NOT NULL DEFAULT 10000,
"tokens_used" INTEGER NOT NULL DEFAULT 0,
"trial_ends_at" TIMESTAMP(3),
"stripe_customer_id" TEXT,
"stripe_subscription_id" TEXT,
"status" "SubscriptionStatus" NOT NULL DEFAULT 'TRIAL',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "orders" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"status" "OrderStatus" NOT NULL DEFAULT 'PAYMENT_CONFIRMED',
"tier" "SubscriptionTier" NOT NULL,
"domain" TEXT NOT NULL,
"tools" TEXT[],
"config_json" JSONB NOT NULL,
"server_ip" TEXT,
"server_password_encrypted" TEXT,
"ssh_port" INTEGER NOT NULL DEFAULT 22,
"portainer_url" TEXT,
"dashboard_url" TEXT,
"failure_reason" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"server_ready_at" TIMESTAMP(3),
"provisioning_started_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
CONSTRAINT "orders_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "provisioning_logs" (
"id" TEXT NOT NULL,
"order_id" TEXT NOT NULL,
"level" "LogLevel" NOT NULL DEFAULT 'INFO',
"message" TEXT NOT NULL,
"step" TEXT,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "provisioning_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "provisioning_jobs" (
"id" TEXT NOT NULL,
"order_id" TEXT NOT NULL,
"job_type" TEXT NOT NULL,
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
"priority" INTEGER NOT NULL DEFAULT 0,
"claimed_at" TIMESTAMP(3),
"claimed_by" TEXT,
"container_name" TEXT,
"attempt" INTEGER NOT NULL DEFAULT 1,
"max_attempts" INTEGER NOT NULL DEFAULT 3,
"next_retry_at" TIMESTAMP(3),
"config_snapshot" JSONB NOT NULL,
"runner_token_hash" TEXT,
"result" JSONB,
"error" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"completed_at" TIMESTAMP(3),
CONSTRAINT "provisioning_jobs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "job_logs" (
"id" TEXT NOT NULL,
"job_id" TEXT NOT NULL,
"level" "LogLevel" NOT NULL DEFAULT 'INFO',
"message" TEXT NOT NULL,
"step" TEXT,
"progress" INTEGER,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "job_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "token_usage" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"instance_id" TEXT,
"operation" TEXT NOT NULL,
"tokens_input" INTEGER NOT NULL,
"tokens_output" INTEGER NOT NULL,
"model" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "token_usage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "runner_tokens" (
"id" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"name" TEXT NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"last_used" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "runner_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "server_connections" (
"id" TEXT NOT NULL,
"order_id" TEXT NOT NULL,
"registration_token" TEXT NOT NULL,
"hub_api_key" TEXT,
"orchestrator_url" TEXT,
"agent_version" TEXT,
"status" "ServerConnectionStatus" NOT NULL DEFAULT 'PENDING',
"registered_at" TIMESTAMP(3),
"last_heartbeat" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "server_connections_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "remote_commands" (
"id" TEXT NOT NULL,
"server_connection_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"status" "CommandStatus" NOT NULL DEFAULT 'PENDING',
"result" JSONB,
"error_message" TEXT,
"queued_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"sent_at" TIMESTAMP(3),
"executed_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
"initiated_by" TEXT,
CONSTRAINT "remote_commands_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "staff_email_key" ON "staff"("email");
-- CreateIndex
CREATE INDEX "provisioning_logs_order_id_timestamp_idx" ON "provisioning_logs"("order_id", "timestamp");
-- CreateIndex
CREATE INDEX "provisioning_jobs_status_priority_created_at_idx" ON "provisioning_jobs"("status", "priority", "created_at");
-- CreateIndex
CREATE INDEX "provisioning_jobs_order_id_idx" ON "provisioning_jobs"("order_id");
-- CreateIndex
CREATE INDEX "job_logs_job_id_timestamp_idx" ON "job_logs"("job_id", "timestamp");
-- CreateIndex
CREATE INDEX "token_usage_user_id_created_at_idx" ON "token_usage"("user_id", "created_at");
-- CreateIndex
CREATE UNIQUE INDEX "runner_tokens_token_hash_key" ON "runner_tokens"("token_hash");
-- CreateIndex
CREATE UNIQUE INDEX "server_connections_order_id_key" ON "server_connections"("order_id");
-- CreateIndex
CREATE UNIQUE INDEX "server_connections_registration_token_key" ON "server_connections"("registration_token");
-- CreateIndex
CREATE UNIQUE INDEX "server_connections_hub_api_key_key" ON "server_connections"("hub_api_key");
-- CreateIndex
CREATE INDEX "remote_commands_server_connection_id_status_idx" ON "remote_commands"("server_connection_id", "status");
-- CreateIndex
CREATE INDEX "remote_commands_status_queued_at_idx" ON "remote_commands"("status", "queued_at");
-- AddForeignKey
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "orders" ADD CONSTRAINT "orders_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "provisioning_logs" ADD CONSTRAINT "provisioning_logs_order_id_fkey" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "provisioning_jobs" ADD CONSTRAINT "provisioning_jobs_order_id_fkey" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "job_logs" ADD CONSTRAINT "job_logs_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "provisioning_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "token_usage" ADD CONSTRAINT "token_usage_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "server_connections" ADD CONSTRAINT "server_connections_order_id_fkey" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "remote_commands" ADD CONSTRAINT "remote_commands_server_connection_id_fkey" FOREIGN KEY ("server_connection_id") REFERENCES "server_connections"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,20 @@
-- AlterTable
ALTER TABLE "orders" ADD COLUMN "company_name" TEXT,
ADD COLUMN "customer" TEXT,
ADD COLUMN "license_key" TEXT;
-- CreateTable
CREATE TABLE "system_settings" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"encrypted" BOOLEAN NOT NULL DEFAULT false,
"category" TEXT NOT NULL DEFAULT 'general',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "system_settings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "system_settings_key_key" ON "system_settings"("key");

View File

@@ -0,0 +1,57 @@
-- CreateEnum
CREATE TYPE "AutomationMode" AS ENUM ('AUTO', 'MANUAL', 'PAUSED');
-- CreateEnum
CREATE TYPE "DnsRecordStatus" AS ENUM ('PENDING', 'VERIFIED', 'MISMATCH', 'NOT_FOUND', 'ERROR', 'SKIPPED');
-- AlterTable
ALTER TABLE "orders" ADD COLUMN "automationMode" "AutomationMode" NOT NULL DEFAULT 'MANUAL',
ADD COLUMN "automation_paused_at" TIMESTAMP(3),
ADD COLUMN "automation_paused_reason" TEXT,
ADD COLUMN "dns_verified_at" TIMESTAMP(3),
ADD COLUMN "netcup_server_id" TEXT,
ADD COLUMN "source" TEXT;
-- CreateTable
CREATE TABLE "dns_verifications" (
"id" TEXT NOT NULL,
"order_id" TEXT NOT NULL,
"wildcard_passed" BOOLEAN NOT NULL DEFAULT false,
"manual_override" BOOLEAN NOT NULL DEFAULT false,
"all_passed" BOOLEAN NOT NULL DEFAULT false,
"total_subdomains" INTEGER NOT NULL DEFAULT 0,
"passed_count" INTEGER NOT NULL DEFAULT 0,
"last_checked_at" TIMESTAMP(3),
"verified_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "dns_verifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "dns_records" (
"id" TEXT NOT NULL,
"dns_verification_id" TEXT NOT NULL,
"subdomain" TEXT NOT NULL,
"full_domain" TEXT NOT NULL,
"expected_ip" TEXT NOT NULL,
"resolved_ip" TEXT,
"status" "DnsRecordStatus" NOT NULL DEFAULT 'PENDING',
"error_message" TEXT,
"checked_at" TIMESTAMP(3),
CONSTRAINT "dns_records_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "dns_verifications_order_id_key" ON "dns_verifications"("order_id");
-- CreateIndex
CREATE INDEX "dns_records_dns_verification_id_idx" ON "dns_records"("dns_verification_id");
-- AddForeignKey
ALTER TABLE "dns_verifications" ADD CONSTRAINT "dns_verifications_order_id_fkey" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "dns_records" ADD CONSTRAINT "dns_records_dns_verification_id_fkey" FOREIGN KEY ("dns_verification_id") REFERENCES "dns_verifications"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "orders" ADD COLUMN "credentials_synced_at" TIMESTAMP(3),
ADD COLUMN "portainer_password_enc" TEXT,
ADD COLUMN "portainer_username" TEXT;

View File

@@ -0,0 +1,143 @@
-- CreateEnum
CREATE TYPE "ErrorSeverity" AS ENUM ('INFO', 'WARNING', 'ERROR', 'CRITICAL');
-- CreateTable
CREATE TABLE "enterprise_clients" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"company_name" TEXT,
"contact_email" TEXT NOT NULL,
"contact_phone" TEXT,
"notes" TEXT,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "enterprise_clients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "enterprise_servers" (
"id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"netcup_server_id" TEXT NOT NULL,
"nickname" TEXT,
"purpose" TEXT,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"portainer_url" TEXT,
"portainer_username" TEXT,
"portainer_password_enc" TEXT,
CONSTRAINT "enterprise_servers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "server_stats_snapshots" (
"id" TEXT NOT NULL,
"server_id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"cpu_percent" DOUBLE PRECISION,
"memory_used_mb" DOUBLE PRECISION,
"memory_total_mb" DOUBLE PRECISION,
"disk_read_mbps" DOUBLE PRECISION,
"disk_write_mbps" DOUBLE PRECISION,
"network_in_mbps" DOUBLE PRECISION,
"network_out_mbps" DOUBLE PRECISION,
"containers_running" INTEGER,
"containers_stopped" INTEGER,
CONSTRAINT "server_stats_snapshots_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "error_detection_rules" (
"id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"pattern" TEXT NOT NULL,
"severity" "ErrorSeverity" NOT NULL DEFAULT 'WARNING',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"description" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "error_detection_rules_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "detected_errors" (
"id" TEXT NOT NULL,
"server_id" TEXT NOT NULL,
"rule_id" TEXT NOT NULL,
"container_id" TEXT,
"container_name" TEXT,
"log_line" TEXT NOT NULL,
"context" TEXT,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"acknowledged_at" TIMESTAMP(3),
"acknowledged_by" TEXT,
CONSTRAINT "detected_errors_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "security_verification_codes" (
"id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"action" TEXT NOT NULL,
"target_server_id" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"used_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "security_verification_codes_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "enterprise_servers_client_id_idx" ON "enterprise_servers"("client_id");
-- CreateIndex
CREATE UNIQUE INDEX "enterprise_servers_client_id_netcup_server_id_key" ON "enterprise_servers"("client_id", "netcup_server_id");
-- CreateIndex
CREATE INDEX "server_stats_snapshots_server_id_timestamp_idx" ON "server_stats_snapshots"("server_id", "timestamp");
-- CreateIndex
CREATE INDEX "server_stats_snapshots_client_id_timestamp_idx" ON "server_stats_snapshots"("client_id", "timestamp");
-- CreateIndex
CREATE INDEX "error_detection_rules_client_id_idx" ON "error_detection_rules"("client_id");
-- CreateIndex
CREATE INDEX "detected_errors_server_id_timestamp_idx" ON "detected_errors"("server_id", "timestamp");
-- CreateIndex
CREATE INDEX "detected_errors_rule_id_timestamp_idx" ON "detected_errors"("rule_id", "timestamp");
-- CreateIndex
CREATE INDEX "security_verification_codes_client_id_code_idx" ON "security_verification_codes"("client_id", "code");
-- AddForeignKey
ALTER TABLE "enterprise_servers" ADD CONSTRAINT "enterprise_servers_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "enterprise_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "server_stats_snapshots" ADD CONSTRAINT "server_stats_snapshots_server_id_fkey" FOREIGN KEY ("server_id") REFERENCES "enterprise_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "server_stats_snapshots" ADD CONSTRAINT "server_stats_snapshots_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "enterprise_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "error_detection_rules" ADD CONSTRAINT "error_detection_rules_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "enterprise_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "detected_errors" ADD CONSTRAINT "detected_errors_server_id_fkey" FOREIGN KEY ("server_id") REFERENCES "enterprise_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "detected_errors" ADD CONSTRAINT "detected_errors_rule_id_fkey" FOREIGN KEY ("rule_id") REFERENCES "error_detection_rules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "security_verification_codes" ADD CONSTRAINT "security_verification_codes_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "enterprise_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,84 @@
-- CreateEnum
CREATE TYPE "ContainerEventType" AS ENUM ('CRASH', 'OOM_KILLED', 'RESTART', 'STOPPED');
-- CreateTable
CREATE TABLE "log_scan_positions" (
"id" TEXT NOT NULL,
"server_id" TEXT NOT NULL,
"container_id" TEXT NOT NULL,
"last_line_count" INTEGER NOT NULL DEFAULT 0,
"last_log_hash" TEXT,
"last_scanned_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "log_scan_positions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "container_state_snapshots" (
"id" TEXT NOT NULL,
"server_id" TEXT NOT NULL,
"container_id" TEXT NOT NULL,
"container_name" TEXT NOT NULL,
"state" TEXT NOT NULL,
"exit_code" INTEGER,
"captured_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "container_state_snapshots_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "container_events" (
"id" TEXT NOT NULL,
"server_id" TEXT NOT NULL,
"container_id" TEXT NOT NULL,
"container_name" TEXT NOT NULL,
"event_type" "ContainerEventType" NOT NULL,
"exit_code" INTEGER,
"details" TEXT,
"acknowledged_at" TIMESTAMP(3),
"acknowledged_by" TEXT,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "container_events_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notification_settings" (
"id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"critical_errors_only" BOOLEAN NOT NULL DEFAULT true,
"container_crashes" BOOLEAN NOT NULL DEFAULT true,
"recipients" TEXT[] DEFAULT ARRAY[]::TEXT[],
"cooldown_minutes" INTEGER NOT NULL DEFAULT 30,
"last_notified_at" TIMESTAMP(3),
CONSTRAINT "notification_settings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "log_scan_positions_server_id_container_id_key" ON "log_scan_positions"("server_id", "container_id");
-- CreateIndex
CREATE INDEX "container_state_snapshots_server_id_container_id_captured_a_idx" ON "container_state_snapshots"("server_id", "container_id", "captured_at");
-- CreateIndex
CREATE INDEX "container_events_server_id_timestamp_idx" ON "container_events"("server_id", "timestamp");
-- CreateIndex
CREATE INDEX "container_events_event_type_acknowledged_at_idx" ON "container_events"("event_type", "acknowledged_at");
-- CreateIndex
CREATE UNIQUE INDEX "notification_settings_client_id_key" ON "notification_settings"("client_id");
-- AddForeignKey
ALTER TABLE "log_scan_positions" ADD CONSTRAINT "log_scan_positions_server_id_fkey" FOREIGN KEY ("server_id") REFERENCES "enterprise_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "container_state_snapshots" ADD CONSTRAINT "container_state_snapshots_server_id_fkey" FOREIGN KEY ("server_id") REFERENCES "enterprise_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "container_events" ADD CONSTRAINT "container_events_server_id_fkey" FOREIGN KEY ("server_id") REFERENCES "enterprise_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notification_settings" ADD CONSTRAINT "notification_settings_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "enterprise_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,46 @@
-- CreateEnum
CREATE TYPE "StaffStatus" AS ENUM ('ACTIVE', 'SUSPENDED');
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "StaffRole" ADD VALUE 'OWNER';
ALTER TYPE "StaffRole" ADD VALUE 'MANAGER';
-- AlterTable
ALTER TABLE "staff" ADD COLUMN "backup_codes_enc" TEXT,
ADD COLUMN "invited_by" TEXT,
ADD COLUMN "status" "StaffStatus" NOT NULL DEFAULT 'ACTIVE',
ADD COLUMN "two_factor_enabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "two_factor_secret_enc" TEXT,
ADD COLUMN "two_factor_verified_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "users" ADD COLUMN "backup_codes_enc" TEXT,
ADD COLUMN "two_factor_enabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "two_factor_secret_enc" TEXT,
ADD COLUMN "two_factor_verified_at" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "staff_invitations" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"role" "StaffRole" NOT NULL DEFAULT 'SUPPORT',
"token" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"invited_by" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "staff_invitations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "staff_invitations_email_key" ON "staff_invitations"("email");
-- CreateIndex
CREATE UNIQUE INDEX "staff_invitations_token_key" ON "staff_invitations"("token");

View File

@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "notification_cooldowns" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"last_sent_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "notification_cooldowns_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "notification_cooldowns_type_key" ON "notification_cooldowns"("type");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "staff" ADD COLUMN "profile_photo_key" TEXT;

View File

@@ -0,0 +1,34 @@
-- AlterTable: Add brute-force attempt tracking to security verification codes
ALTER TABLE "security_verification_codes" ADD COLUMN "attempts" INTEGER NOT NULL DEFAULT 0;
-- AlterTable: Add hash-based API key lookup to server connections
ALTER TABLE "server_connections" ADD COLUMN "hub_api_key_hash" TEXT;
-- CreateTable: DB-backed 2FA sessions (replacing in-memory Map)
CREATE TABLE "pending_2fa_sessions" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"user_type" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"role" TEXT,
"company" TEXT,
"subscription" JSONB,
"expires_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "pending_2fa_sessions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "pending_2fa_sessions_token_key" ON "pending_2fa_sessions"("token");
-- CreateIndex
CREATE INDEX "pending_2fa_sessions_token_idx" ON "pending_2fa_sessions"("token");
-- CreateIndex
CREATE INDEX "pending_2fa_sessions_expires_at_idx" ON "pending_2fa_sessions"("expires_at");
-- CreateIndex
CREATE UNIQUE INDEX "server_connections_hub_api_key_hash_key" ON "server_connections"("hub_api_key_hash");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@@ -0,0 +1,743 @@
// 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 configured in prisma.config.mjs (Prisma 7+)
}
// ============================================================================
// ENUMS
// ============================================================================
enum UserStatus {
PENDING_VERIFICATION
ACTIVE
SUSPENDED
}
enum StaffRole {
OWNER // Full access, cannot be deleted
ADMIN // Full access, can manage staff
MANAGER // Orders + servers, no staff/settings
SUPPORT // View only + limited actions
}
enum StaffStatus {
ACTIVE
SUSPENDED
}
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 AutomationMode {
AUTO // Website orders - self-executing
MANUAL // Staff-created - step-by-step
PAUSED // Stopped for intervention
}
enum DnsRecordStatus {
PENDING
VERIFIED
MISMATCH
NOT_FOUND
ERROR
SKIPPED // For wildcard pass or manual override
}
enum JobStatus {
PENDING
CLAIMED
RUNNING
COMPLETED
FAILED
DEAD
}
enum LogLevel {
DEBUG
INFO
WARN
ERROR
}
enum ServerConnectionStatus {
PENDING // Awaiting orchestrator registration
REGISTERED // Orchestrator has registered
ONLINE // Recent heartbeat received
OFFLINE // No recent heartbeat
}
enum CommandStatus {
PENDING
SENT
EXECUTING
COMPLETED
FAILED
}
// ============================================================================
// SYSTEM SETTINGS
// ============================================================================
model SystemSetting {
id String @id @default(cuid())
key String @unique
value String // Encrypted for sensitive values
encrypted Boolean @default(false)
category String @default("general")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("system_settings")
}
// ============================================================================
// 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")
// 2FA fields
twoFactorEnabled Boolean @default(false) @map("two_factor_enabled")
twoFactorSecretEnc String? @map("two_factor_secret_enc")
twoFactorVerifiedAt DateTime? @map("two_factor_verified_at")
backupCodesEnc String? @map("backup_codes_enc")
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)
status StaffStatus @default(ACTIVE)
invitedBy String? @map("invited_by") // Staff ID who sent invite
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Profile
profilePhotoKey String? @map("profile_photo_key") // S3/MinIO key for profile photo
// 2FA fields
twoFactorEnabled Boolean @default(false) @map("two_factor_enabled")
twoFactorSecretEnc String? @map("two_factor_secret_enc")
twoFactorVerifiedAt DateTime? @map("two_factor_verified_at")
backupCodesEnc String? @map("backup_codes_enc")
@@map("staff")
}
model StaffInvitation {
id String @id @default(cuid())
email String @unique
role StaffRole @default(SUPPORT)
token String @unique
expiresAt DateTime @map("expires_at")
invitedBy String @map("invited_by")
createdAt DateTime @default(now()) @map("created_at")
@@map("staff_invitations")
}
// ============================================================================
// 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")
// Automation mode
automationMode AutomationMode @default(MANUAL)
automationPausedAt DateTime? @map("automation_paused_at")
automationPausedReason String? @map("automation_paused_reason")
source String? // "website" | "staff" | "api"
// Customer/provisioning config
customer String? @map("customer") // Short name for subdomains (e.g., "acme")
companyName String? @map("company_name") // Display name (e.g., "Acme Corporation")
licenseKey String? @map("license_key") // Generated: lb_inst_xxx
// Server credentials (entered by staff)
serverIp String? @map("server_ip")
serverPasswordEncrypted String? @map("server_password_encrypted")
sshPort Int @default(22) @map("ssh_port")
netcupServerId String? @map("netcup_server_id") // Netcup API server ID for linking
// Generated after provisioning
portainerUrl String? @map("portainer_url")
dashboardUrl String? @map("dashboard_url")
failureReason String? @map("failure_reason")
// Portainer credentials (encrypted, synced from agent)
portainerUsername String? @map("portainer_username") // e.g., "admin-xyz123"
portainerPasswordEnc String? @map("portainer_password_enc") // AES-256-CBC encrypted
credentialsSyncedAt DateTime? @map("credentials_synced_at") // Last sync from agent
// 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")
dnsVerifiedAt DateTime? @map("dns_verified_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
provisioningLogs ProvisioningLog[]
jobs ProvisioningJob[]
serverConnection ServerConnection?
dnsVerification DnsVerification?
@@map("orders")
}
// ============================================================================
// DNS VERIFICATION
// ============================================================================
model DnsVerification {
id String @id @default(cuid())
orderId String @unique @map("order_id")
wildcardPassed Boolean @default(false) @map("wildcard_passed")
manualOverride Boolean @default(false) @map("manual_override") // Staff skipped check
allPassed Boolean @default(false) @map("all_passed")
totalSubdomains Int @default(0) @map("total_subdomains")
passedCount Int @default(0) @map("passed_count")
lastCheckedAt DateTime? @map("last_checked_at")
verifiedAt DateTime? @map("verified_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
records DnsRecord[]
@@map("dns_verifications")
}
model DnsRecord {
id String @id @default(cuid())
dnsVerificationId String @map("dns_verification_id")
subdomain String // "cloud"
fullDomain String @map("full_domain") // "cloud.example.com"
expectedIp String @map("expected_ip")
resolvedIp String? @map("resolved_ip")
status DnsRecordStatus @default(PENDING)
errorMessage String? @map("error_message")
checkedAt DateTime? @map("checked_at")
dnsVerification DnsVerification @relation(fields: [dnsVerificationId], references: [id], onDelete: Cascade)
@@index([dnsVerificationId])
@@map("dns_records")
}
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")
}
// ============================================================================
// SERVER CONNECTION (Phone-Home System)
// ============================================================================
model ServerConnection {
id String @id @default(cuid())
orderId String @unique @map("order_id")
// Registration token (generated during provisioning, used by orchestrator to register)
registrationToken String @unique @map("registration_token")
// Hub API key (issued after successful registration, used for heartbeats/commands)
hubApiKey String? @unique @map("hub_api_key")
hubApiKeyHash String? @unique @map("hub_api_key_hash")
// Orchestrator connection info (provided during registration)
orchestratorUrl String? @map("orchestrator_url")
agentVersion String? @map("agent_version")
// Status tracking
status ServerConnectionStatus @default(PENDING)
registeredAt DateTime? @map("registered_at")
lastHeartbeat DateTime? @map("last_heartbeat")
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
remoteCommands RemoteCommand[]
@@map("server_connections")
}
// ============================================================================
// REMOTE COMMANDS (Support Backdoor)
// ============================================================================
model RemoteCommand {
id String @id @default(cuid())
serverConnectionId String @map("server_connection_id")
// Command details
type String // SHELL, RESTART_SERVICE, UPDATE, ECHO, etc.
payload Json // Command-specific payload
// Execution tracking
status CommandStatus @default(PENDING)
result Json? // Command result
errorMessage String? @map("error_message")
// Timestamps
queuedAt DateTime @default(now()) @map("queued_at")
sentAt DateTime? @map("sent_at")
executedAt DateTime? @map("executed_at")
completedAt DateTime? @map("completed_at")
// Staff who initiated (for audit)
initiatedBy String? @map("initiated_by")
serverConnection ServerConnection @relation(fields: [serverConnectionId], references: [id], onDelete: Cascade)
@@index([serverConnectionId, status])
@@index([status, queuedAt])
@@map("remote_commands")
}
// ============================================================================
// ENTERPRISE CLIENTS
// ============================================================================
enum ErrorSeverity {
INFO
WARNING
ERROR
CRITICAL
}
model EnterpriseClient {
id String @id @default(cuid())
name String
companyName String? @map("company_name")
contactEmail String @map("contact_email") // For security codes
contactPhone String? @map("contact_phone")
notes String? @db.Text
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
servers EnterpriseServer[]
errorRules ErrorDetectionRule[]
securityCodes SecurityVerificationCode[]
statsHistory ServerStatsSnapshot[]
notificationSetting NotificationSetting?
@@map("enterprise_clients")
}
model EnterpriseServer {
id String @id @default(cuid())
clientId String @map("client_id")
netcupServerId String @map("netcup_server_id") // Link to Netcup server
nickname String? // Optional friendly name
purpose String? // e.g., "Production", "Staging", "Database"
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Portainer credentials (encrypted)
portainerUrl String? @map("portainer_url")
portainerUsername String? @map("portainer_username")
portainerPasswordEnc String? @map("portainer_password_enc")
// Relations
client EnterpriseClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
statsSnapshots ServerStatsSnapshot[]
errorLogs DetectedError[]
logScanPositions LogScanPosition[]
stateSnapshots ContainerStateSnapshot[]
containerEvents ContainerEvent[]
@@unique([clientId, netcupServerId])
@@index([clientId])
@@map("enterprise_servers")
}
// ============================================================================
// ENTERPRISE STATS HISTORY (90-day retention)
// ============================================================================
model ServerStatsSnapshot {
id String @id @default(cuid())
serverId String @map("server_id")
clientId String @map("client_id")
timestamp DateTime @default(now())
// Server metrics (from Netcup)
cpuPercent Float? @map("cpu_percent")
memoryUsedMb Float? @map("memory_used_mb")
memoryTotalMb Float? @map("memory_total_mb")
diskReadMbps Float? @map("disk_read_mbps")
diskWriteMbps Float? @map("disk_write_mbps")
networkInMbps Float? @map("network_in_mbps")
networkOutMbps Float? @map("network_out_mbps")
// Container summary
containersRunning Int? @map("containers_running")
containersStopped Int? @map("containers_stopped")
// Relations
server EnterpriseServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
client EnterpriseClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@index([serverId, timestamp])
@@index([clientId, timestamp])
@@map("server_stats_snapshots")
}
// ============================================================================
// ERROR DETECTION
// ============================================================================
model ErrorDetectionRule {
id String @id @default(cuid())
clientId String @map("client_id")
name String // e.g., "Database Connection Failed"
pattern String // Regex pattern
severity ErrorSeverity @default(WARNING)
isActive Boolean @default(true) @map("is_active")
description String? // What this rule detects
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
client EnterpriseClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
detectedErrors DetectedError[]
@@index([clientId])
@@map("error_detection_rules")
}
model DetectedError {
id String @id @default(cuid())
serverId String @map("server_id")
ruleId String @map("rule_id")
containerId String? @map("container_id") // Optional: which container
containerName String? @map("container_name")
logLine String @db.Text @map("log_line") // The actual log line that matched
context String? @db.Text // Surrounding log context
timestamp DateTime @default(now())
acknowledgedAt DateTime? @map("acknowledged_at")
acknowledgedBy String? @map("acknowledged_by") // User ID who acknowledged
// Relations
server EnterpriseServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
rule ErrorDetectionRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
@@index([serverId, timestamp])
@@index([ruleId, timestamp])
@@map("detected_errors")
}
// ============================================================================
// SECURITY VERIFICATION (for destructive actions)
// ============================================================================
model SecurityVerificationCode {
id String @id @default(cuid())
clientId String @map("client_id")
code String // 8-digit code
action String // "WIPE" | "REINSTALL"
targetServerId String @map("target_server_id") // Which server
expiresAt DateTime @map("expires_at")
usedAt DateTime? @map("used_at")
attempts Int @default(0) // Failed verification attempts
createdAt DateTime @default(now()) @map("created_at")
// Relations
client EnterpriseClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@index([clientId, code])
@@map("security_verification_codes")
}
// ============================================================================
// INTELLIGENT ERROR TRACKING
// ============================================================================
enum ContainerEventType {
CRASH // Was running, now exited with non-zero exit code
OOM_KILLED // Out of memory kill
RESTART // Container restarted
STOPPED // Intentional stop (exit code 0 or manual)
}
// Track log scanning position to avoid re-scanning same content
model LogScanPosition {
id String @id @default(cuid())
serverId String @map("server_id")
containerId String @map("container_id")
lastLineCount Int @default(0) @map("last_line_count")
lastLogHash String? @map("last_log_hash") // Detect log rotation
lastScannedAt DateTime @default(now()) @map("last_scanned_at")
server EnterpriseServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
@@unique([serverId, containerId])
@@map("log_scan_positions")
}
// Track container state over time for crash detection
model ContainerStateSnapshot {
id String @id @default(cuid())
serverId String @map("server_id")
containerId String @map("container_id")
containerName String @map("container_name")
state String // "running", "exited", "dead"
exitCode Int? @map("exit_code")
capturedAt DateTime @default(now()) @map("captured_at")
server EnterpriseServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
@@index([serverId, containerId, capturedAt])
@@map("container_state_snapshots")
}
// Record significant container lifecycle events
model ContainerEvent {
id String @id @default(cuid())
serverId String @map("server_id")
containerId String @map("container_id")
containerName String @map("container_name")
eventType ContainerEventType @map("event_type")
exitCode Int? @map("exit_code")
details String? @db.Text
acknowledgedAt DateTime? @map("acknowledged_at")
acknowledgedBy String? @map("acknowledged_by")
timestamp DateTime @default(now())
server EnterpriseServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
@@index([serverId, timestamp])
@@index([eventType, acknowledgedAt])
@@map("container_events")
}
// Email notification settings per client
model NotificationSetting {
id String @id @default(cuid())
clientId String @unique @map("client_id")
enabled Boolean @default(false)
criticalErrorsOnly Boolean @default(true) @map("critical_errors_only")
containerCrashes Boolean @default(true) @map("container_crashes")
recipients String[] @default([])
cooldownMinutes Int @default(30) @map("cooldown_minutes")
lastNotifiedAt DateTime? @map("last_notified_at")
client EnterpriseClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@map("notification_settings")
}
// ============================================================================
// PENDING 2FA SESSIONS
// ============================================================================
model Pending2FASession {
id String @id @default(cuid())
token String @unique
userId String @map("user_id")
userType String @map("user_type") // 'customer' | 'staff'
email String
name String?
role String? // StaffRole for staff users
company String?
subscription Json? // Subscription data for customer users
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
@@index([token])
@@index([expiresAt])
@@map("pending_2fa_sessions")
}
// ============================================================================
// SYSTEM-WIDE NOTIFICATION COOLDOWN
// ============================================================================
// Track last notification time per notification type for system-wide cooldown
model NotificationCooldown {
id String @id @default(cuid())
type String @unique // e.g., "container_crash", "critical_error", "stats_cpu"
lastSentAt DateTime @map("last_sent_at")
@@map("notification_cooldowns")
}

320
letsbe-hub/prisma/seed.ts Normal file
View 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()
})