import Fastify, { FastifyInstance, FastifyError } from 'fastify'; import cors from '@fastify/cors'; import { NominatimClient, NominatimResultSchema } from './services/NominatimClient'; import { GithubClient, SponsorsResponseSchema } from './services/GithubClient'; const start = async () => { const server: FastifyInstance = Fastify({ logger: { level: 'error', transport: { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, }, }, trustProxy: true, }); // Global error handler server.setErrorHandler((error: FastifyError, request, reply) => { server.log.error({ url: request.url, method: request.method, error: error.message, stack: error.stack, }, 'Request error'); reply.status(error.statusCode || 500).send({ error: 'Internal Server Error', }); }); // Coors Light Config await server.register(cors, { origin: (origin, cb) => { const allowedOrigins = [ 'http://localhost:5173', // vite dev server 'https://deflock.org', 'https://www.deflock.org' ]; if (!origin || allowedOrigins.includes(origin) || /^https:\/\/.*\.deflock\.pages\.dev$/.test(origin)) { cb(null, true); } else { cb(null, false); } }, methods: ['GET', 'HEAD'], }); const nominatim = new NominatimClient(); const githubClient = new GithubClient(); const shutdown = async () => { server.log.info("Shutting down"); await server.close(); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); server.get('/geocode', { schema: { querystring: { type: 'object', properties: { query: { type: 'string' }, }, required: ['query'], }, response: { 200: NominatimResultSchema, 500: { type: 'object', properties: { error: { type: 'string' } } }, }, }, }, async (request, reply) => { const { query } = request.query as { query: string }; reply.header('Cache-Control', 'public, max-age=300, s-maxage=86400'); const result = await nominatim.geocodeSingleResult(query); return result; }); server.get('/sponsors/github', { schema: { querystring: { type: 'object', properties: { username: { type: 'string', default: 'frillweeman' }, }, }, response: { 200: SponsorsResponseSchema, 500: { type: 'object', properties: { error: { type: 'string' } } }, }, }, }, async (request, reply) => { const { username } = request.query as { username?: string }; reply.header('Cache-Control', 'public, max-age=60, s-maxage=600'); const result = await githubClient.getSponsors(username || 'frillweeman'); return result; }); server.head('/healthcheck', async (request, reply) => { reply.status(200).send(); }); try { await server.listen({ host: '0.0.0.0', port: 3000 }); console.log('Server listening on port 3000'); } catch (err) { console.error('Failed to start server:', err); server.log.error(err); process.exit(1); } }; start().catch(err => { console.error('Fatal error:', err); process.exit(1); });