diff --git a/CLAUDE.md b/CLAUDE.md index de6ffde..98b7465 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,12 +32,12 @@ npx wrangler tail ``` src/ -├── index.ts # Main router, CORS, request handling +├── index.ts # Main router, CORS, Queue consumer ├── config.ts # Configuration constants ├── types.ts # TypeScript type definitions ├── region-utils.ts # Region matching utilities ├── utils.ts # Re-exports from utils/ (backward compatibility) -├── utils/ # Modular utilities (split from monolithic utils.ts) +├── utils/ # Modular utilities │ ├── index.ts # Central export point │ ├── http.ts # HTTP responses, CORS, escapeHtml │ ├── validation.ts # Input validation, type guards @@ -46,14 +46,21 @@ src/ │ ├── ai.ts # AI prompt sanitization │ └── exchange-rate.ts # Currency conversion ├── repositories/ -│ └── AnvilServerRepository.ts # DB queries for Anvil servers +│ ├── AnvilServerRepository.ts # DB queries for Anvil servers +│ └── ProvisioningRepository.ts # Users, deposits, orders (telegram-conversations) ├── services/ -│ └── ai-service.ts # AI recommendations & fallback logic +│ ├── ai-service.ts # AI recommendations & fallback logic +│ ├── provisioning-service.ts # Server provisioning workflow +│ ├── vps-provider.ts # VPS provider abstract base class +│ ├── linode-provider.ts # Linode API implementation +│ └── vultr-provider.ts # Vultr API implementation ├── handlers/ │ ├── health.ts # GET /api/health │ ├── servers.ts # GET /api/servers │ ├── recommend.ts # POST /api/recommend -│ └── report.ts # GET /api/recommend/report +│ ├── report.ts # GET /api/recommend/report +│ ├── provision.ts # POST/GET/DELETE /api/provision/* +│ └── queue.ts # Queue consumer for async provisioning └── __tests__/ ├── utils.test.ts # Validation & security tests (27 tests) └── bandwidth.test.ts # Bandwidth estimation tests (32 tests, including CDN) @@ -84,6 +91,13 @@ src/ **Legacy tables (no longer used)**: - `providers`, `instance_types`, `pricing`, `regions` - Old Linode/Vultr data +### D1 Database Tables (telegram-conversations) + +**User & Payment tables**: +- `users` - Telegram users (id, telegram_id, username) +- `user_deposits` - User balance in KRW (user_id, balance) +- `server_orders` - Server provisioning orders (status, price_paid, provider_instance_id, ip_address) + ## Key Implementation Details ### DB Workload Multiplier (`recommend.ts`) @@ -254,6 +268,62 @@ When OpenAI API is unavailable, the system automatically falls back to rule-base - Provides basic capacity estimates based on vCPU count - Warns user that AI is temporarily unavailable +### Server Provisioning API (`handlers/provision.ts`) + +Queue-based async server provisioning with automatic balance management. + +**Endpoints** (require `X-API-Key` header): +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/provision` | POST | Create server order (sends to Queue) | +| `/api/provision/orders` | GET | List user's orders | +| `/api/provision/orders/:id` | GET | Get order details (includes root_password) | +| `/api/provision/orders/:id` | DELETE | Terminate server | +| `/api/provision/balance` | GET | Get user balance (KRW) | + +**Provisioning Flow**: +``` +POST /api/provision + ↓ +1. Validate user (telegram_id) +2. Get pricing (anvil_pricing) +3. Calculate KRW price (exchange rate × 500원 rounding) +4. Deduct balance (atomic query) +5. Create order (status: provisioning) +6. Send to Queue → Return immediately + ↓ +[Queue Consumer] + ↓ +7. Fetch order (get root_password from DB) +8. Call provider API (Linode/Vultr) +9. Wait for IP assignment (polling, max 2min) +10. Update order (status: active, ip_address) + ↓ +On failure: Refund balance + status: failed +``` + +**Request Body**: +```json +{ + "user_id": "telegram_id", + "pricing_id": 26, + "label": "my-server", + "image": "ubuntu_22_04", + "dry_run": false +} +``` + +**Security Features**: +- API key authentication (`X-API-Key` header) +- Origin validation for browser requests +- Atomic balance deduction (prevents race condition) +- Root password stored in DB only (not in Queue message) +- Automatic refund on any failure + +**Supported Providers**: +- Linode (`linode-provider.ts`) +- Vultr (`vultr-provider.ts`) + ### Configuration (`config.ts`) Centralized limits and constants: @@ -280,8 +350,23 @@ binding = "DB" database_name = "cloud-instances-db" database_id = "bbcb472d-b25e-4e48-b6ea-112f9fffb4a8" -[vars] -OPENAI_API_KEY = "sk-..." # Set via wrangler secret +[[d1_databases]] +binding = "USER_DB" +database_name = "telegram-conversations" +database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e" + +[[queues.producers]] +queue = "provision-queue" +binding = "PROVISION_QUEUE" + +[[queues.consumers]] +queue = "provision-queue" +max_batch_size = 1 +max_retries = 3 +dead_letter_queue = "provision-queue-dlq" + +# Secrets (via wrangler secret put) +# OPENAI_API_KEY, LINODE_API_KEY, VULTR_API_KEY, PROVISION_API_KEY ``` ## Testing @@ -320,6 +405,26 @@ RESULT=$(curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/re REPORT_URL="https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend/report?data=$(echo $RESULT | base64 | tr -d '\n')&lang=ko" # 3. Open in browser or fetch echo $REPORT_URL + +# === Provisioning API (requires X-API-Key header) === + +# Dry run - validate without creating server +curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision -H "Content-Type: application/json" -H "X-API-Key: $PROVISION_API_KEY" -d '{"user_id":"821596605","pricing_id":26,"label":"test","dry_run":true}' | jq . + +# Create server (async via Queue) +curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision -H "Content-Type: application/json" -H "X-API-Key: $PROVISION_API_KEY" -d '{"user_id":"821596605","pricing_id":26,"label":"my-server"}' | jq . + +# Get user balance +curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/balance?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . + +# List user orders +curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/orders?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . + +# Get specific order (includes root_password) +curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/orders/15?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . + +# Terminate server +curl -s -X DELETE "https://cloud-orchestrator.kappa-d8e.workers.dev/api/provision/orders/15?user_id=821596605" -H "X-API-Key: $PROVISION_API_KEY" | jq . ``` ## Recent Changes diff --git a/schema-provisioning.sql b/schema-provisioning.sql new file mode 100644 index 0000000..4d1b2b9 --- /dev/null +++ b/schema-provisioning.sql @@ -0,0 +1,70 @@ +-- Provisioning System Schema +-- Users, Payment Holds, Server Orders + +-- 1. Users table with deposit balance +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, -- UUID + email TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + balance_usd REAL NOT NULL DEFAULT 0.0 CHECK(balance_usd >= 0), + status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'suspended', 'deleted')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_status ON users(status); + +-- 2. Payment holds table (hold → capture/release) +CREATE TABLE IF NOT EXISTS payment_holds ( + id TEXT PRIMARY KEY, -- UUID + user_id TEXT NOT NULL, + order_id TEXT NOT NULL, -- References server_orders.id + amount_usd REAL NOT NULL CHECK(amount_usd > 0), + status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'captured', 'released')), + reason TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + resolved_at TEXT, -- When captured or released + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_payment_holds_user ON payment_holds(user_id); +CREATE INDEX IF NOT EXISTS idx_payment_holds_order ON payment_holds(order_id); +CREATE INDEX IF NOT EXISTS idx_payment_holds_status ON payment_holds(status); + +-- 3. Server orders table +CREATE TABLE IF NOT EXISTS server_orders ( + id TEXT PRIMARY KEY, -- UUID + user_id TEXT NOT NULL, + pricing_id INTEGER NOT NULL, -- References pricing.id (instance_type + region) + provider_name TEXT NOT NULL, -- linode, vultr, etc. + status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ( + 'pending', -- Order created, hold placed + 'provisioning', -- API call in progress + 'active', -- Server running + 'failed', -- Provisioning failed + 'deleted' -- Server terminated + )), + -- Provider response data + provider_instance_id TEXT, -- Linode/Vultr instance ID + server_ip TEXT, -- IPv4 address + server_ipv6 TEXT, -- IPv6 address + root_password TEXT, -- Encrypted/hashed + -- Cost tracking + monthly_cost_usd REAL NOT NULL, + -- Metadata + label TEXT, -- User-defined server label + os_image TEXT NOT NULL DEFAULT 'ubuntu_22_04', + error_message TEXT, + -- Timestamps + created_at TEXT NOT NULL DEFAULT (datetime('now')), + provisioned_at TEXT, + deleted_at TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (pricing_id) REFERENCES pricing(id) +); + +CREATE INDEX IF NOT EXISTS idx_server_orders_user ON server_orders(user_id); +CREATE INDEX IF NOT EXISTS idx_server_orders_status ON server_orders(status); +CREATE INDEX IF NOT EXISTS idx_server_orders_provider ON server_orders(provider_name, provider_instance_id); +CREATE INDEX IF NOT EXISTS idx_server_orders_created ON server_orders(created_at);