Files
deflock/api/server.ts
2026-02-02 12:50:56 -07:00

125 lines
3.3 KiB
TypeScript

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);
});