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>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user