Scaffold Regress: Fastify+SQLite server and Vue frontend for the Ingress-style BTC Map capture game
- Server: NIP-98 nostr auth (session cookies), BTC Map sync, claim/link/field routes, physical-presence checks via geolocation distance. 31 tests passing. - Frontend: Leaflet map, team pick, claim/link UI, Archipelago identity bridge vendored (nostr-provider.js) for dashboard launch support. - Verified end-to-end against the live BTC Map API (167 real Madeira places) and via genuine NIP-98-signed HTTP requests against the built app.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import cookie from '@fastify/cookie';
|
||||
import cors from '@fastify/cors';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { Config } from './config.js';
|
||||
import { openDatabase, type DB } from './db/database.js';
|
||||
import nostrAuth from './plugins/nostr-auth.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import placesRoutes from './routes/places.js';
|
||||
import claimsRoutes from './routes/claims.js';
|
||||
import linksRoutes from './routes/links.js';
|
||||
import fieldsRoutes from './routes/fields.js';
|
||||
import syncRoutes from './routes/sync.js';
|
||||
|
||||
export interface AppContext {
|
||||
config: Config;
|
||||
db: DB;
|
||||
}
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
ctx: AppContext;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BuildAppOptions {
|
||||
config: Config;
|
||||
dbPath?: string; // override for tests (':memory:')
|
||||
logger?: boolean;
|
||||
}
|
||||
|
||||
export async function buildApp(opts: BuildAppOptions): Promise<FastifyInstance> {
|
||||
const { config } = opts;
|
||||
const db = openDatabase(opts.dbPath ?? join(config.DATA_DIR, 'regress.sqlite3'));
|
||||
|
||||
const app = Fastify({ logger: opts.logger ?? true, bodyLimit: 1 * 1024 * 1024 });
|
||||
|
||||
const ctx: AppContext = { config, db };
|
||||
app.decorate('ctx', ctx);
|
||||
|
||||
await app.register(cookie);
|
||||
await app.register(cors, { origin: true, credentials: true, methods: ['GET', 'POST', 'DELETE'] });
|
||||
await app.register(nostrAuth, { db, config });
|
||||
|
||||
app.get('/api/health', async () => ({ status: 'ok' }));
|
||||
|
||||
await app.register(authRoutes);
|
||||
await app.register(placesRoutes);
|
||||
await app.register(claimsRoutes);
|
||||
await app.register(linksRoutes);
|
||||
await app.register(fieldsRoutes);
|
||||
await app.register(syncRoutes);
|
||||
|
||||
const staticDir = config.STATIC_DIR;
|
||||
if (staticDir && existsSync(staticDir)) {
|
||||
await app.register(fastifyStatic, { root: staticDir });
|
||||
app.setNotFoundHandler((req, reply) => {
|
||||
if (req.raw.url?.startsWith('/api/')) {
|
||||
return reply.code(404).send({ error: 'not found' });
|
||||
}
|
||||
return reply.sendFile('index.html');
|
||||
});
|
||||
}
|
||||
|
||||
app.addHook('onClose', async () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
Reference in New Issue
Block a user