Include full contents of all nested repositories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
272
letsbe-hub/prisma/migrations/20260106134433_init/migration.sql
Normal file
272
letsbe-hub/prisma/migrations/20260106134433_init/migration.sql
Normal 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;
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "staff" ADD COLUMN "profile_photo_key" TEXT;
|
||||
@@ -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");
|
||||
3
letsbe-hub/prisma/migrations/migration_lock.toml
Normal file
3
letsbe-hub/prisma/migrations/migration_lock.toml
Normal 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"
|
||||
743
letsbe-hub/prisma/schema.prisma
Normal file
743
letsbe-hub/prisma/schema.prisma
Normal 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
320
letsbe-hub/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