import { Router } from 'express'; import { env } from '../env.js'; import { query, one } from '../db.js'; import { ensureBuckets, BUCKETS } from '../lib/storage.js'; import { analyzeWebsite } from './analyze-website.js'; import { triggerCloudBuild, getCloudBuildStatus } from './cloud-build.js'; import type { AuthedRequest } from '../middleware/auth.js'; /** * Server-side functions (payments, build, email, AI, storage helpers). * Reachable at both POST /api/functions/:name (primary) and * GET|POST /api/functions/v1/:name (alias used by some admin screens). */ const r = Router(); type Ctx = { body: any; query: any; req: AuthedRequest }; type Handler = (ctx: Ctx) => Promise; const NOT_CONFIGURED = (gateway: string) => new Error(`${gateway} is not configured. Add the API keys to your server .env to enable it.`); const handlers: Record = { 'analyze-website': async ({ body }) => { const url = body?.websiteUrl || body?.url; if (!url) throw new Error('websiteUrl is required'); return analyzeWebsite(url); }, 'cloud-build': async ({ body }) => triggerCloudBuild(body), 'cloud-build-status': async ({ body }) => { const status = await getCloudBuildStatus(body?.buildId); if (!status) throw new Error('Build not found'); return status; }, 'send-email': async ({ body }) => { if (!env.RESEND_API_KEY) { console.log('[send-email] (stub) would send:', body?.to, body?.subject); return { success: true, stubbed: true, message: 'Email logged (Resend not configured)' }; } const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { Authorization: `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: body.from || 'AppForge ', to: body.to, subject: body.subject, html: body.html || body.htmlContent, }), }); const json = await res.json(); if (!res.ok) throw new Error(json?.message || 'Email send failed'); return { success: true, id: json.id }; }, 'test-storage-connection': async () => { ensureBuckets(); return { success: true, message: 'Local storage is available', provider: 'local-filesystem' }; }, 'storage-admin': async ({ query: q }) => { ensureBuckets(); const action = q?.action || 'list'; if (action === 'list') { return BUCKETS.map((name) => ({ id: name, name, public: name !== 'project-assets', file_size_limit: null, created_at: new Date(0).toISOString(), })); } // create/delete are no-ops for fixed local buckets return { success: true, message: `Local storage uses fixed buckets; '${action}' is a no-op.` }; }, 'ai-assistant': async ({ body }) => { const messages = body?.messages || (body?.message ? [{ role: 'user', content: body.message }] : []); const apiKey = env.OPENAI_API_KEY; if (!apiKey) { return { reply: 'The AI assistant is not configured. Add OPENAI_API_KEY to the server .env to enable it.', stubbed: true }; } const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4o-mini', messages, temperature: 0.7 }), }); const json = await res.json(); if (!res.ok) throw new Error(json?.error?.message || 'AI request failed'); return { reply: json.choices?.[0]?.message?.content || '' }; }, 'reset-demo-data': async () => ({ success: true, message: 'Demo data reset not applicable in self-hosted mode' }), 'retry-webhook': async ({ body }) => { if (!body?.id) throw new Error('Webhook log id required'); await query(`UPDATE public.webhook_event_logs SET status = 'retried' WHERE id = $1`, [body.id]); return { success: true }; }, 'stripe-checkout': async () => { throw NOT_CONFIGURED('Stripe'); }, 'stripe-portal': async () => { throw NOT_CONFIGURED('Stripe'); }, 'stripe-webhook': async () => ({ received: true }), 'paypal-checkout': async () => { throw NOT_CONFIGURED('PayPal'); }, 'paypal-billing': async () => { throw NOT_CONFIGURED('PayPal'); }, 'paypal-webhook': async () => ({ received: true }), 'coinbase-checkout': async () => { throw NOT_CONFIGURED('Coinbase'); }, 'coinbase-webhook': async () => ({ received: true }), }; const PUBLIC = new Set(['cloud-build-status', 'test-storage-connection', 'storage-admin', 'stripe-webhook', 'paypal-webhook', 'coinbase-webhook']); async function dispatch(req: AuthedRequest, res: any) { const name = req.params.name; // health probe used by edge-function-health.ts if (req.query?.health) return res.json({ ok: true, function: name }); const handler = handlers[name]; if (!handler) return res.status(404).json({ error: `Unknown function: ${name}` }); if (!PUBLIC.has(name) && !req.userId) return res.status(401).json({ error: 'Not authenticated' }); try { const result = await handler({ body: req.body || {}, query: req.query || {}, req }); res.json(result ?? null); } catch (err: any) { res.status(400).json({ error: err?.message || 'Function failed' }); } } r.post('/:name', dispatch); r.get('/:name', dispatch); r.post('/v1/:name', dispatch); r.get('/v1/:name', dispatch); export default r;