Files
appforge/server/src/routes/builds.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

75 lines
2.5 KiB
TypeScript

import { Router } from 'express';
import { query, one } from '../db.js';
import { insertRow } from '../lib/sql.js';
import { h } from '../lib/respond.js';
import { requireAuth, type AuthedRequest } from '../middleware/auth.js';
import { triggerCloudBuild, getCloudBuildStatus } from '../functions/cloud-build.js';
const r = Router();
r.use(requireAuth);
const uid = (req: AuthedRequest) => req.userId as string;
function sanitizeSegment(segment: string, fallback: string) {
const cleaned = segment.toLowerCase().replace(/[^a-z0-9_]/g, '');
if (!cleaned) return fallback;
return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
}
function sanitizePackageName(raw: string | undefined, appName: string) {
const fallback = ['com', 'app', sanitizeSegment(appName, 'mobile')];
const src = (raw || '').split('.').filter(Boolean);
const segs = src.length ? src.map((s, i) => sanitizeSegment(s, fallback[i] || 'app')) : fallback;
while (segs.length < 3) segs.push(fallback[segs.length] || 'app');
return segs.join('.');
}
// POST /builds -> start a build
r.post('/', h(async (req) => {
const config = req.body || {};
const platform = config.platform || 'android';
const appName = config.appName || 'My App';
const packageName = sanitizePackageName(config.packageName, appName);
const normalizedConfig = { ...config, packageName };
const build = await insertRow<any>('app_builds', {
user_id: uid(req as any),
app_name: appName,
package_name: packageName,
website_url: config.websiteUrl,
config: normalizedConfig,
status: 'pending',
progress: 0,
});
try {
const result = await triggerCloudBuild({
buildId: build.id,
websiteUrl: config.websiteUrl,
appName,
platform,
packageName,
config: normalizedConfig,
});
return { buildId: build.id, cloudBuildId: result.cloudBuildId, message: result.message };
} catch (e: any) {
return { buildId: build.id, message: 'Build created but cloud trigger failed: ' + e.message };
}
}));
// GET /builds?limit=
r.get('/', h(async (req) => {
const limit = Math.min(parseInt(String(req.query.limit ?? '20'), 10) || 20, 200);
return query(
`SELECT * FROM public.app_builds WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2`,
[uid(req as any), limit]
);
}));
// GET /builds/:id/status
r.get('/:id/status', h(async (req) => {
const status = await getCloudBuildStatus(req.params.id);
if (status) return status;
return one(`SELECT * FROM public.app_builds WHERE id = $1 AND user_id = $2`, [req.params.id, uid(req as any)]);
}));
export default r;