// ═══════════════════════════════════════════════════════════════ // IX SAAS BACKEND v2.1 — Inference-X Build Platform // All config via environment variables — zero hardcode // ═══════════════════════════════════════════════════════════════ 'use strict'; const express = require('express'); const cors = require('cors'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const path = require('path'); const fs = require('fs'); const https = require('https'); require('dotenv').config({ path: '/opt/ix-saas/.env' }); // ── CONFIG (ALL from .env) ────────────────────────────────────── const PORT = process.env.PORT || 4080; const JWT_SECRET = process.env.JWT_SECRET || (() => { throw new Error('JWT_SECRET required'); })(); const DB_PATH = process.env.DB_PATH || '/opt/ix-saas/data/saas.db'; const ADMIN_EMAIL = process.env.ADMIN_EMAIL || ''; const OC_API_KEY = process.env.ONECLOUD_API_KEY || ''; const OC_CLIENT_KEY = process.env.ONECLOUD_CLIENT_KEY || ''; const STRIPE_SECRET = process.env.STRIPE_SECRET_KEY || ''; const STRIPE_WEBHOOK_SIG = process.env.STRIPE_WEBHOOK_SECRET || ''; const STRIPE_PRICE_PRO = process.env.STRIPE_PRICE_PRO || ''; const STRIPE_PRICE_BIZ = process.env.STRIPE_PRICE_BUSINESS || ''; const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || ''; const PAYPAL_SECRET = process.env.PAYPAL_CLIENT_SECRET || ''; const BASE_URL = process.env.BASE_URL || 'https://build.inference-x.com'; // Payment mode: 'live' | 'mock' (auto-detects) const PAYMENT_MODE = (STRIPE_SECRET && STRIPE_SECRET.startsWith('sk_live_')) ? 'live' : 'mock'; // ── DATABASE ──────────────────────────────────────────────────── const Database = require('better-sqlite3'); const db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, name TEXT, plan TEXT DEFAULT 'free', builds_count INTEGER DEFAULT 0, region TEXT DEFAULT 'eu', language TEXT DEFAULT 'en', stripe_customer_id TEXT, stripe_subscription_id TEXT, paypal_subscription_id TEXT, instance_id TEXT, instance_ip TEXT, instance_status TEXT DEFAULT 'none', store_seller INTEGER DEFAULT 0, store_revenue REAL DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), last_active TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS builds ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, model_id TEXT, hardware TEXT, quant TEXT, language TEXT DEFAULT 'en', system_prompt TEXT, personality TEXT DEFAULT 'concise', is_public INTEGER DEFAULT 0, store_listed INTEGER DEFAULT 0, store_price REAL DEFAULT 0, downloads INTEGER DEFAULT 0, rating REAL DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, plan TEXT NOT NULL, status TEXT DEFAULT 'active', provider TEXT, provider_subscription_id TEXT, current_period_end TEXT, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS instances ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, onecloud_id TEXT, plan TEXT, status TEXT DEFAULT 'pending', ip TEXT, region TEXT, location_city TEXT, vcpu INTEGER, ram_gb INTEGER, disk_gb INTEGER, cost_hourly REAL, created_at TEXT DEFAULT (datetime('now')), last_active TEXT DEFAULT (datetime('now')), snapshot_id TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS store_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, build_id INTEGER NOT NULL, seller_id INTEGER NOT NULL, name TEXT NOT NULL, description TEXT, category TEXT DEFAULT 'general', price REAL DEFAULT 0, is_free INTEGER DEFAULT 1, downloads INTEGER DEFAULT 0, rating REAL DEFAULT 0, rating_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY(build_id) REFERENCES builds(id), FOREIGN KEY(seller_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS store_purchases ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER NOT NULL, buyer_id INTEGER NOT NULL, seller_id INTEGER NOT NULL, price REAL, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS store_reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER NOT NULL, user_id INTEGER NOT NULL, rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5), comment TEXT, created_at TEXT DEFAULT (datetime('now')), UNIQUE(item_id, user_id) ); CREATE TABLE IF NOT EXISTS enterprise_leads ( id INTEGER PRIMARY KEY AUTOINCREMENT, company TEXT, email TEXT NOT NULL, use_case TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS mock_payments ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, plan TEXT NOT NULL, amount REAL, currency TEXT DEFAULT 'USD', provider TEXT DEFAULT 'mock', session_id TEXT, status TEXT DEFAULT 'pending', created_at TEXT DEFAULT (datetime('now')) ); `); // ── PLAN CATALOG ───────────────────────────────────────────────── const PLANS = { free: { builds: 3, api_calls: 0, instance: null, store_sell: false }, studio_test: { builds: -1, api_calls: 10000, instance: { vcpu: 2, ram: 2, disk: 50, size_id: '87' }, store_sell: false }, lab_test: { builds: -1, api_calls: 100000, instance: { vcpu: 4, ram: 8, disk: 125, size_id: '88' }, store_sell: true }, enterprise_test: { builds: -1, api_calls: -1, instance: { vcpu: 8, ram: 32, disk: 300, size_id: '89' }, store_sell: true }, pro: { builds: -1, api_calls: 10000, instance: { vcpu: 2, ram: 2, disk: 50, size_id: '87' }, store_sell: false }, business: { builds: -1, api_calls: 100000, instance: { vcpu: 4, ram: 8, disk: 125, size_id: '88' }, store_sell: true }, enterprise: { builds: -1, api_calls: -1, instance: { vcpu: 8, ram: 32, disk: 300, size_id: '89' }, store_sell: true }, }; // OneCloud location → nearest city (real IDs from API) const REGION_TO_LOCATION = { eu: { city: 'Frankfurt', id: '34' }, us: { city: 'New York', id: '6' }, ap: { city: 'Singapore', id: '55' }, mena: { city: 'Fez', id: '198' }, sa: { city: 'São Paulo', id: '2' }, }; function detectRegion(ip) { if (!ip) return 'eu'; const first = parseInt((ip || '').split('.')[0], 10); if ([196,197,41,105].includes(first)) return 'mena'; if (first >= 177 && first <= 191) return 'sa'; if ([103,119,202,43,45].includes(first)) return 'ap'; if (first >= 3 && first <= 52 && first % 2 === 1) return 'us'; return 'eu'; } // ── ONECLOUD API ───────────────────────────────────────────────── function ocRequest(method, endpoint, params = {}) { return new Promise((resolve) => { if (!OC_API_KEY || !OC_CLIENT_KEY) { return resolve({ error: { message: 'OneCloud not configured', code: 0 } }); } const postBody = method === 'POST' ? Object.entries(params) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&') : ''; const getQuery = method === 'GET' && Object.keys(params).length ? '?' + Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&') : ''; const options = { hostname: 'api.oneprovider.com', path: endpoint + getQuery, method, headers: { 'Api-Key': OC_API_KEY, 'Client-Key': OC_CLIENT_KEY, 'User-Agent': 'OneApi/1.0', 'Content-Type': 'application/x-www-form-urlencoded', }, }; if (postBody) options.headers['Content-Length'] = Buffer.byteLength(postBody); const req = https.request(options, (res) => { let data = ''; res.on('data', c => data += c); res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } }); }); req.on('error', (e) => resolve({ error: { message: e.message } })); if (postBody) req.write(postBody); req.end(); }); } // Instance boot script (no sensitive data) function bootScript(plan) { return `#!/bin/bash export DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y -qq curl wget nginx curl -sL https://inference-x.com/install.sh | bash useradd -m -s /bin/bash ixuser 2>/dev/null || true systemctl enable nginx && systemctl start nginx curl -sX POST ${BASE_URL}/api/instance/ready \\ -H "Content-Type: application/json" \\ -d "{\"label\":\"$(hostname)\",\"plan\":\"${plan}\"}" `; } async function provisionInstance(userId, plan, region) { const planCfg = PLANS[plan]; if (!planCfg?.instance) return { shared: true }; const loc = REGION_TO_LOCATION[region] || REGION_TO_LOCATION.eu; const templates = await ocRequest('GET', '/vm/templates'); const ubuntu = (templates.response || []).find(t => (t.name||'').toLowerCase().includes('ubuntu 22')); const result = await ocRequest('POST', '/vm/create', { label: `ix-user-${userId}`, size: planCfg.instance.size_id, location: loc.id, template: ubuntu ? ubuntu.id : 'ubuntu-22', script: bootScript(plan), }); if (result.response?.id) { db.prepare(`INSERT OR REPLACE INTO instances (user_id, onecloud_id, plan, status, region, location_city, vcpu, ram_gb, disk_gb) VALUES (?, ?, ?, 'provisioning', ?, ?, ?, ?, ?) `).run(userId, result.response.id, plan, region, loc.city, planCfg.instance.vcpu, planCfg.instance.ram, planCfg.instance.disk); db.prepare(`UPDATE users SET instance_id=?, instance_status='provisioning' WHERE id=?`) .run(result.response.id, userId); } return result; } // ── MOCK PAYMENT ───────────────────────────────────────────────── function createMockSession(userId, plan) { const prices = { pro: 0, business: 0, enterprise: 0, studio_test: 0, lab_test: 0, enterprise_test: 0 }; const sessionId = 'mock_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10); db.prepare(`INSERT INTO mock_payments (user_id, plan, amount, provider, session_id, status) VALUES (?, ?, ?, 'mock', ?, 'pending')` ).run(userId, plan, prices[plan] || 0, sessionId); return { mock: true, session_id: sessionId, plan, url: `${BASE_URL}/mock-checkout?session=${sessionId}&plan=${plan}` }; } async function completeMockPayment(sessionId) { const payment = db.prepare(`SELECT * FROM mock_payments WHERE session_id=?`).get(sessionId); if (!payment) return null; db.prepare(`UPDATE mock_payments SET status='completed' WHERE session_id=?`).run(sessionId); db.prepare(`UPDATE users SET plan=? WHERE id=?`).run(payment.plan, payment.user_id); const u = db.prepare(`SELECT * FROM users WHERE id=?`).get(payment.user_id); if (u) await provisionInstance(payment.user_id, payment.plan, u.region || 'eu'); return payment; } // ── APP ─────────────────────────────────────────────────────────── const app = express(); app.use(cors({ origin: true, credentials: true })); // Stripe webhook needs raw body app.use('/api/billing/stripe-webhook', express.raw({ type: 'application/json' })); app.use(express.json({ limit: '5mb' })); // Security headers app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); next(); }); // ── MIDDLEWARE ──────────────────────────────────────────────────── function auth(req, res, next) { const h = req.headers.authorization || ''; if (!h.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' }); try { req.user = jwt.verify(h.slice(7), JWT_SECRET); db.prepare(`UPDATE users SET last_active=datetime('now') WHERE id=?`).run(req.user.id); next(); } catch { res.status(401).json({ error: 'Invalid token' }); } } function adminOnly(req, res, next) { auth(req, res, () => { if (!ADMIN_EMAIL || req.user.email !== ADMIN_EMAIL) return res.status(403).json({ error: 'Admin only' }); next(); }); } function requirePlan(...plans) { return (req, res, next) => { auth(req, res, () => { const u = db.prepare('SELECT plan FROM users WHERE id=?').get(req.user.id); if (!u || !plans.includes(u.plan)) return res.status(403).json({ error: `Requires: ${plans.join(' or ')}`, upgrade: true }); next(); }); }; } // ── AUTH ────────────────────────────────────────────────────────── app.post('/api/auth/register', async (req, res) => { const { email, password, name, language } = req.body; if (!email || !password) return res.status(400).json({ error: 'Email and password required' }); if (password.length < 8) return res.status(400).json({ error: 'Password too short (8+ chars)' }); try { const hash = await bcrypt.hash(password, 10); const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip || ''; const region = detectRegion(ip); const u = db.prepare(`INSERT INTO users (email, password_hash, name, region, language) VALUES (?, ?, ?, ?, ?)`) .run(email.toLowerCase().trim(), hash, name || email.split('@')[0], region, language || 'en'); const token = jwt.sign({ id: u.lastInsertRowid, email: email.toLowerCase() }, JWT_SECRET, { expiresIn: '30d' }); res.json({ token, user: { id: u.lastInsertRowid, email: email.toLowerCase(), name, plan: 'free', region } }); } catch (e) { if (e.message?.includes('UNIQUE')) return res.status(409).json({ error: 'Email already used' }); res.status(500).json({ error: 'Registration failed' }); } }); app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; const u = db.prepare('SELECT * FROM users WHERE email=?').get((email||'').toLowerCase().trim()); if (!u || !await bcrypt.compare(password, u.password_hash)) return res.status(401).json({ error: 'Invalid email or password' }); const token = jwt.sign({ id: u.id, email: u.email }, JWT_SECRET, { expiresIn: '30d' }); res.json({ token, user: { id: u.id, email: u.email, name: u.name, plan: u.plan, region: u.region, instance_status: u.instance_status } }); }); app.get('/api/auth/me', auth, (req, res) => { const u = db.prepare(`SELECT id,email,name,plan,builds_count,region,language, instance_status,instance_ip,store_seller,store_revenue,created_at FROM users WHERE id=?`).get(req.user.id); if (!u) return res.status(404).json({ error: 'Not found' }); res.json(u); }); // ── BUILDS ─────────────────────────────────────────────────────── app.get('/api/builds', auth, (req, res) => { res.json(db.prepare('SELECT * FROM builds WHERE user_id=? ORDER BY created_at DESC').all(req.user.id)); }); app.post('/api/builds', auth, (req, res) => { const u = db.prepare('SELECT plan, builds_count FROM users WHERE id=?').get(req.user.id); const limit = PLANS[u.plan]?.builds ?? 3; if (limit !== -1 && u.builds_count >= limit) return res.status(403).json({ error: `Plan limit: ${limit} builds. Upgrade to continue.`, upgrade: true }); const { name, model_id, hardware, quant, language, system_prompt, personality } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Build name required' }); const b = db.prepare(`INSERT INTO builds (user_id,name,model_id,hardware,quant,language,system_prompt,personality) VALUES (?,?,?,?,?,?,?,?)`).run(req.user.id, name.trim(), model_id, hardware, quant, language, system_prompt, personality); db.prepare('UPDATE users SET builds_count=builds_count+1 WHERE id=?').run(req.user.id); res.json({ id: b.lastInsertRowid, name: name.trim() }); }); app.delete('/api/builds/:id', auth, (req, res) => { const b = db.prepare('SELECT id FROM builds WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!b) return res.status(404).json({ error: 'Build not found' }); db.prepare('DELETE FROM builds WHERE id=?').run(req.params.id); db.prepare('UPDATE users SET builds_count=MAX(0,builds_count-1) WHERE id=?').run(req.user.id); res.json({ ok: true }); }); // ── INSTANCES ──────────────────────────────────────────────────── app.get('/api/instance', auth, (req, res) => { const inst = db.prepare('SELECT * FROM instances WHERE user_id=?').get(req.user.id); res.json(inst || { status: 'none' }); }); app.post('/api/instance/provision', auth, async (req, res) => { const u = db.prepare('SELECT plan, region FROM users WHERE id=?').get(req.user.id); if (u.plan === 'free') return res.status(403).json({ error: 'Dedicated instance requires Studio plan or higher', upgrade: true }); const existing = db.prepare('SELECT status FROM instances WHERE user_id=?').get(req.user.id); if (existing && existing.status !== 'destroyed') return res.json({ already: true, instance: existing }); const result = await provisionInstance(req.user.id, u.plan, u.region || 'eu'); res.json({ ok: true, result }); }); app.post('/api/instance/ready', (req, res) => { const { label, plan } = req.body; if (label) { db.prepare(`UPDATE instances SET status='running' WHERE onecloud_id=?`).run(label); db.prepare(`UPDATE users SET instance_status='running' WHERE instance_id=?`).run(label); } res.json({ ok: true }); }); app.post('/api/instance/stop', auth, async (req, res) => { const inst = db.prepare('SELECT onecloud_id FROM instances WHERE user_id=?').get(req.user.id); if (!inst?.onecloud_id) return res.status(404).json({ error: 'No instance' }); await ocRequest('POST', '/vm/shutdown', { vm_id: inst.onecloud_id }); db.prepare(`UPDATE instances SET status='stopped' WHERE user_id=?`).run(req.user.id); db.prepare(`UPDATE users SET instance_status='stopped' WHERE id=?`).run(req.user.id); res.json({ ok: true }); }); app.post('/api/instance/start', auth, async (req, res) => { const inst = db.prepare('SELECT onecloud_id FROM instances WHERE user_id=?').get(req.user.id); if (!inst?.onecloud_id) return res.status(404).json({ error: 'No instance' }); await ocRequest('POST', '/vm/boot', { vm_id: inst.onecloud_id }); db.prepare(`UPDATE instances SET status='running' WHERE user_id=?`).run(req.user.id); db.prepare(`UPDATE users SET instance_status='running' WHERE id=?`).run(req.user.id); res.json({ ok: true }); }); app.post('/api/instance/snapshot', auth, async (req, res) => { const inst = db.prepare('SELECT onecloud_id FROM instances WHERE user_id=?').get(req.user.id); if (!inst?.onecloud_id) return res.status(404).json({ error: 'No instance' }); const r = await ocRequest('POST', '/vm/image/create', { vm_id: inst.onecloud_id, label: `ix-snap-${req.user.id}-${Date.now()}` }); if (r.response?.id) db.prepare(`UPDATE instances SET snapshot_id=? WHERE user_id=?`).run(r.response.id, req.user.id); res.json({ ok: true, snapshot: r.response }); }); // ── STORE ──────────────────────────────────────────────────────── app.get('/api/store', (req, res) => { const { category, sort = 'downloads', limit = 20, page = 0 } = req.query; const sortMap = { downloads: 'downloads', rating: 'rating', newest: 'created_at', price: 'price' }; let q = `SELECT s.id,s.name,s.description,s.category,s.price,s.is_free,s.downloads,s.rating,s.rating_count, s.status,s.created_at,u.name as seller_name,b.model_id,b.personality FROM store_items s JOIN users u ON s.seller_id=u.id JOIN builds b ON s.build_id=b.id WHERE s.status='approved'`; const params = []; if (category && category !== 'all') { q += ' AND s.category=?'; params.push(category); } q += ` ORDER BY s.${sortMap[sort]||'downloads'} DESC LIMIT ? OFFSET ?`; params.push(parseInt(limit)||20, parseInt(page)*parseInt(limit)||0); res.json(db.prepare(q).all(...params)); }); app.get('/api/store/:id', (req, res) => { const item = db.prepare(` SELECT s.*,u.name as seller_name,b.model_id,b.personality FROM store_items s JOIN users u ON s.seller_id=u.id JOIN builds b ON s.build_id=b.id WHERE s.id=? AND s.status='approved' `).get(req.params.id); if (!item) return res.status(404).json({ error: 'Not found' }); const reviews = db.prepare(`SELECT r.rating,r.comment,r.created_at,u.name FROM store_reviews r JOIN users u ON r.user_id=u.id WHERE r.item_id=? ORDER BY r.created_at DESC LIMIT 20`).all(req.params.id); res.json({ ...item, reviews }); }); app.post('/api/store/publish', auth, requirePlan('business', 'enterprise'), (req, res) => { const { build_id, name, description, category, price, is_free } = req.body; const build = db.prepare('SELECT id FROM builds WHERE id=? AND user_id=?').get(build_id, req.user.id); if (!build) return res.status(404).json({ error: 'Build not found' }); if (!name?.trim()) return res.status(400).json({ error: 'Name required' }); const item = db.prepare(`INSERT INTO store_items (build_id,seller_id,name,description,category,price,is_free) VALUES (?,?,?,?,?,?,?)`).run(build_id, req.user.id, name.trim(), description, category||'general', price||0, is_free?1:0); db.prepare('UPDATE users SET store_seller=1 WHERE id=?').run(req.user.id); res.json({ id: item.lastInsertRowid, status: 'pending', message: 'Submitted for review (24-48h)' }); }); app.post('/api/store/:id/download', auth, (req, res) => { const item = db.prepare('SELECT * FROM store_items WHERE id=? AND status=?').get(req.params.id, 'approved'); if (!item) return res.status(404).json({ error: 'Not found' }); if (!item.is_free && item.price > 0) { const bought = db.prepare('SELECT id FROM store_purchases WHERE item_id=? AND buyer_id=?').get(req.params.id, req.user.id); if (!bought) return res.status(402).json({ error: 'Purchase required', price: item.price }); } db.prepare('UPDATE store_items SET downloads=downloads+1 WHERE id=?').run(req.params.id); if (!item.is_free && item.price > 0) db.prepare('UPDATE users SET store_revenue=store_revenue+? WHERE id=?').run(item.price * 0.8, item.seller_id); const build = db.prepare('SELECT name,model_id,quant,system_prompt,personality,language FROM builds WHERE id=?').get(item.build_id); res.json({ build, item: { id: item.id, name: item.name, category: item.category } }); }); app.post('/api/store/:id/review', auth, (req, res) => { const { rating, comment } = req.body; if (!rating || rating < 1 || rating > 5) return res.status(400).json({ error: 'Rating 1-5 required' }); try { db.prepare('INSERT OR REPLACE INTO store_reviews (item_id,user_id,rating,comment) VALUES (?,?,?,?)') .run(req.params.id, req.user.id, rating, comment||''); const avg = db.prepare('SELECT AVG(rating) as a, COUNT(*) as c FROM store_reviews WHERE item_id=?').get(req.params.id); db.prepare('UPDATE store_items SET rating=?,rating_count=? WHERE id=?').run(avg.a, avg.c, req.params.id); res.json({ ok: true, rating: avg.a }); } catch (e) { res.status(500).json({ error: 'Review failed' }); } }); // ── BILLING ─────────────────────────────────────────────────────── app.post('/api/billing/create-session', auth, async (req, res) => { const { plan } = req.body; if (!PLANS[plan] || plan === 'free') return res.status(400).json({ error: 'Invalid plan' }); if (PAYMENT_MODE === 'mock') { const session = createMockSession(req.user.id, plan); return res.json(session); } try { const Stripe = require('stripe')(STRIPE_SECRET); const prices = { pro: STRIPE_PRICE_PRO, business: STRIPE_PRICE_BIZ }; const session = await Stripe.checkout.sessions.create({ mode: 'subscription', payment_method_types: ['card'], line_items: [{ price: prices[plan], quantity: 1 }], success_url: `${BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${BASE_URL}/#pricing`, metadata: { user_id: String(req.user.id), plan }, }); res.json({ url: session.url }); } catch (e) { res.status(500).json({ error: 'Payment session failed' }); } }); // Mock checkout completion app.post('/api/billing/mock-complete', async (req, res) => { const { session_id } = req.body; if (!session_id?.startsWith('mock_')) return res.status(400).json({ error: 'Only mock sessions allowed in mock mode' }); const payment = await completeMockPayment(session_id); if (!payment) return res.status(404).json({ error: 'Session not found' }); res.json({ ok: true, plan: payment.plan, user_id: payment.user_id }); }); app.get('/api/billing/status', auth, (req, res) => { const sub = db.prepare('SELECT * FROM subscriptions WHERE user_id=?').get(req.user.id); const mock = db.prepare(`SELECT * FROM mock_payments WHERE user_id=? AND status='completed' ORDER BY id DESC LIMIT 1`).get(req.user.id); res.json({ subscription: sub, mock_payment: mock, mode: PAYMENT_MODE }); }); // Stripe webhook app.post('/api/billing/stripe-webhook', async (req, res) => { if (PAYMENT_MODE !== 'live' || !STRIPE_WEBHOOK_SIG) return res.json({ ok: true }); try { const Stripe = require('stripe')(STRIPE_SECRET); const event = Stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], STRIPE_WEBHOOK_SIG); if (event.type === 'checkout.session.completed') { const { user_id, plan } = event.data.object.metadata; db.prepare('UPDATE users SET plan=? WHERE id=?').run(plan, user_id); const u = db.prepare('SELECT * FROM users WHERE id=?').get(user_id); if (u) await provisionInstance(Number(user_id), plan, u.region||'eu'); } if (event.type === 'customer.subscription.deleted') { const u = db.prepare('SELECT * FROM users WHERE stripe_subscription_id=?').get(event.data.object.id); if (u) { db.prepare('UPDATE users SET plan=? WHERE id=?').run('free', u.id); const inst = db.prepare('SELECT onecloud_id FROM instances WHERE user_id=?').get(u.id); if (inst?.onecloud_id) { await ocRequest('POST', '/vm/image/create', { vm_id: inst.onecloud_id, label: `ix-final-${u.id}` }); await ocRequest('POST', '/vm/destroy', { vm_id: inst.onecloud_id }); db.prepare(`UPDATE instances SET status='destroyed' WHERE user_id=?`).run(u.id); } } } res.json({ received: true }); } catch (e) { res.status(400).json({ error: e.message }); } }); // ── ADMIN ──────────────────────────────────────────────────────── app.get('/api/admin/stats', adminOnly, (req, res) => { const u = db.prepare('SELECT count(*) as t FROM users').get().t; const f = db.prepare("SELECT count(*) as t FROM users WHERE plan='free'").get().t; const p = db.prepare("SELECT count(*) as t FROM users WHERE plan='pro'").get().t; const b = db.prepare("SELECT count(*) as t FROM users WHERE plan='business'").get().t; const e = db.prepare("SELECT count(*) as t FROM users WHERE plan='enterprise'").get().t; const mrr = p*49 + b*199 + e*999; const builds = db.prepare('SELECT COALESCE(sum(builds_count),0) as t FROM users').get().t; const items = db.prepare("SELECT count(*) as t FROM store_items WHERE status='approved'").get().t; const instances= db.prepare("SELECT count(*) as t FROM instances WHERE status='running'").get().t; const revenue = db.prepare('SELECT COALESCE(sum(store_revenue),0) as t FROM users').get().t; const mocks = db.prepare("SELECT count(*) as t FROM mock_payments WHERE status='completed'").get().t; res.json({ users:u, free:f, pro:p, business:b, enterprise:e, mrr, arr:mrr*12, builds, store_items:items, instances, seller_revenue:revenue, mock_completions:mocks, payment_mode:PAYMENT_MODE }); }); app.get('/api/admin/users', adminOnly, (req, res) => { res.json(db.prepare('SELECT id,email,name,plan,builds_count,region,instance_status,store_seller,created_at FROM users ORDER BY created_at DESC LIMIT 200').all()); }); app.get('/api/admin/instances', adminOnly, (req, res) => { res.json(db.prepare('SELECT i.*,u.email,u.plan FROM instances i JOIN users u ON i.user_id=u.id ORDER BY i.created_at DESC').all()); }); app.get('/api/admin/store', adminOnly, (req, res) => { res.json(db.prepare(`SELECT s.*,u.email as seller_email FROM store_items s JOIN users u ON s.seller_id=u.id ORDER BY s.created_at DESC`).all()); }); app.post('/api/admin/store/:id/approve', adminOnly, (req, res) => { db.prepare("UPDATE store_items SET status='approved' WHERE id=?").run(req.params.id); res.json({ ok: true }); }); app.post('/api/admin/store/:id/reject', adminOnly, (req, res) => { db.prepare("UPDATE store_items SET status='rejected' WHERE id=?").run(req.params.id); res.json({ ok: true }); }); // ── MISC ────────────────────────────────────────────────────────── app.post('/api/contact/enterprise', (req, res) => { const { company, email, use_case } = req.body; if (!email) return res.status(400).json({ error: 'Email required' }); db.prepare('INSERT INTO enterprise_leads (company,email,use_case) VALUES (?,?,?)').run(company, email, use_case); res.json({ ok: true }); }); app.get('/api/health', (req, res) => { const stats = db.prepare('SELECT count(*) as t FROM users').get(); res.json({ status: 'ok', service: 'ix-saas', version: '2.1.0', users: stats.t, payment_mode: PAYMENT_MODE, ts: new Date().toISOString() }); }); // Mock checkout page app.get('/mock-checkout', (req, res) => { const { session, plan } = req.query; const prices = { pro: 0, business: 0, enterprise: 0, studio_test: 0, lab_test: 0, enterprise_test: 0 }; res.send(`
Plan: ${plan}
Test plan — free activation — no real charge.