100 lines
3.9 KiB
TypeScript
100 lines
3.9 KiB
TypeScript
/**
|
|
* The one server.
|
|
*
|
|
* One Fastify instance on `127.0.0.1:8431` serving `/api/v1/*`, behind Caddy,
|
|
* with one systemd unit and one Caddy snippet. Three separate backends were
|
|
* designed for this box — three ports, three frameworks, three deploy files that
|
|
* would have overwritten each other — and this is the one that replaces them.
|
|
* Weather, flights, markers and offices are route modules here, not services on
|
|
* ports of their own. CONTRACT.md §5.
|
|
*
|
|
* `buildApp` never listens. Tests build one and use `inject()`; `index.ts` builds
|
|
* one and binds it.
|
|
*/
|
|
|
|
import Fastify, { type FastifyInstance } from "fastify";
|
|
import { registerCachePolicy } from "./cache.ts";
|
|
import { loadConfig, type Config } from "./config.ts";
|
|
import { registerFlights } from "./routes/flights.ts";
|
|
import { registerHealth } from "./routes/health.ts";
|
|
import { registerMarkers } from "./routes/markers.ts";
|
|
import { registerMedia } from "./routes/media.ts";
|
|
import { registerOffices } from "./routes/offices.ts";
|
|
import { registerPresence } from "./routes/presence.ts";
|
|
import { registerRealtime } from "./routes/realtime.ts";
|
|
import { registerSatellites } from "./routes/satellites.ts";
|
|
import { registerSession } from "./routes/session.ts";
|
|
import { registerWeather } from "./routes/weather.ts";
|
|
import { createServices } from "./services.ts";
|
|
import type { ErrorBody } from "../../src/server/wire.ts";
|
|
|
|
export function buildApp(config: Config = loadConfig()): FastifyInstance {
|
|
const app = Fastify({
|
|
logger: { level: config.logLevel },
|
|
// Caddy is the only thing that talks to this socket, so its X-Forwarded-For
|
|
// is the client address. Nothing else can reach the port to forge one.
|
|
trustProxy: true,
|
|
});
|
|
|
|
// One loud line per demotion, at boot, before anything can be served. The
|
|
// health body carries the same list for anyone without log access.
|
|
for (const line of config.degraded) {
|
|
app.log.warn(`TERA DEGRADED: ${line}`);
|
|
}
|
|
|
|
registerCachePolicy(app);
|
|
registerCors(app, config.corsOrigins);
|
|
|
|
const services = createServices(config, app.log);
|
|
registerHealth(app, services);
|
|
registerFlights(app, services);
|
|
registerSatellites(app, services);
|
|
registerWeather(app, services);
|
|
registerMarkers(app, services);
|
|
registerMedia(app, services);
|
|
registerOffices(app, services);
|
|
registerPresence(app, services);
|
|
registerRealtime(app, services);
|
|
registerSession(app, services);
|
|
|
|
app.setNotFoundHandler(async (_req, reply) => {
|
|
const body: ErrorBody = { error: "not_found", message: "No such route." };
|
|
return reply.code(404).send(body);
|
|
});
|
|
|
|
app.setErrorHandler(async (err, _req, reply) => {
|
|
app.log.error({ err }, "unhandled error");
|
|
const body: ErrorBody = { error: "internal", message: "Something went wrong." };
|
|
return reply.code(500).send(body);
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
/**
|
|
* CORS, by hand.
|
|
*
|
|
* The default is an empty allowlist — the browser build is served from the same
|
|
* origin through Caddy, so nothing needs this until somebody runs the dev server
|
|
* on a different port and sets `TERA_CORS_ORIGIN`. A plugin whose entire job is
|
|
* fifteen lines of header-setting is not worth the dependency, and the fifteen
|
|
* lines being visible here is worth something on a route that decides who may
|
|
* read the API from where.
|
|
*/
|
|
function registerCors(app: FastifyInstance, allowed: string[]): void {
|
|
if (allowed.length === 0) return;
|
|
|
|
app.addHook("onRequest", async (req, reply) => {
|
|
const origin = req.headers.origin;
|
|
if (typeof origin !== "string" || !allowed.includes(origin)) return;
|
|
|
|
reply.header("access-control-allow-origin", origin);
|
|
reply.header("access-control-allow-headers", "authorization, content-type");
|
|
reply.header("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
// Whatever the response ends up being, it depended on the Origin header.
|
|
reply.header("vary", "Origin");
|
|
|
|
if (req.method === "OPTIONS") await reply.code(204).send();
|
|
});
|
|
}
|