Files
appforge/server/src/functions/index.ts
T
Leon SerfatyandClaude Opus 4.8 1a8b8ee5a0 AppForge: migrate off Supabase to external Postgres + Better Auth + local storage
- New Express + TypeScript backend (server/) with pg, Better Auth, local file storage
- De-Supabased Postgres schema (server/db) and TS reimplementations of DB functions
- Frontend data layer rewired to REST (rest-client + backend-client compat shim)
- Removed all Supabase references (code, config, deps, docs)
- New brand assets: gradient favicon/app icons + dark/white wordmark logos

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 04:50:29 -04:00

137 lines
5.3 KiB
TypeScript

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<any>;
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<string, Handler> = {
'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 <onboarding@resend.dev>',
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;