Files
dpu-api/src/index.ts
2026-03-04 16:27:05 +01:00

252 lines
6.7 KiB
TypeScript

import { WsService } from "@dpu/shared";
import { logError } from "@dpu/shared/dist/logger.js";
import fastifyCors, { type FastifyCorsOptions } from "@fastify/cors";
import fastifySwagger, { type SwaggerOptions } from "@fastify/swagger";
import fastifySwaggerUi, {
type FastifySwaggerUiOptions,
} from "@fastify/swagger-ui";
import fastifyWebsocket from "@fastify/websocket";
import type { FastifyReply, FastifyRequest } from "fastify";
import Fastify from "fastify";
import fastifyAxios, { type FastifyAxiosOptions } from "fastify-axios";
import {
jsonSchemaTransform,
serializerCompiler,
validatorCompiler,
type ZodTypeProvider,
} from "fastify-type-provider-zod";
import type { OpenAPIV3 } from "openapi-types";
import { SwaggerTheme, SwaggerThemeNameEnum } from "swagger-themes";
import { z } from "zod";
import { Config } from "./config.js";
import { GadgetbridgeClient } from "./gadgetbridge/client.js";
import { privateGadgetbridgeRoutes } from "./gadgetbridge/private-routes.js";
import { GadgetbridgeService } from "./gadgetbridge/service.js";
import { GristClient } from "./grist/client.js";
import { privateGristRoutes } from "./grist/private-routes.js";
import { GristService } from "./grist/service.js";
import { HomeAssistantClient } from "./homeassistant/client.js";
import { privateHomeAssistantRoutes } from "./homeassistant/private-routes.js";
import { HomeAssistantService } from "./homeassistant/service.js";
import { privateHomepageRoutes } from "./homepage/private-routes.js";
import { HomepageService } from "./homepage/service.js";
import { TidalClient } from "./tidal/client.js";
import { privateTidalRoutes } from "./tidal/private-routes.js";
import { TidalService } from "./tidal/service.js";
import { publicWsRoutes } from "./websocket/public-routes.js";
// Initialize with Zod
const publicServer = Fastify().withTypeProvider<ZodTypeProvider>();
const privateServer = Fastify().withTypeProvider<ZodTypeProvider>();
publicServer.setValidatorCompiler(validatorCompiler);
publicServer.setSerializerCompiler(serializerCompiler);
privateServer.setValidatorCompiler(validatorCompiler);
privateServer.setSerializerCompiler(serializerCompiler);
// Cors
function getCorsObject(urls: string[]): FastifyCorsOptions {
return {
origin: urls,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
};
}
await publicServer.register(
fastifyCors,
getCorsObject(["https://dariusbag.dev"]),
);
await privateServer.register(
fastifyCors,
getCorsObject(["http://localhost:4321"]),
);
// Swagger
function getSwaggerObject(servers: OpenAPIV3.ServerObject[]): SwaggerOptions {
return {
openapi: {
info: {
title: "DPU API",
description: "API Documentation",
version: "1.0.0",
},
servers: servers,
},
transform: jsonSchemaTransform,
};
}
await publicServer.register(
fastifySwagger,
getSwaggerObject([
{ url: "https://dpu.dariusbag.dev/api", description: "prod" },
]),
);
await privateServer.register(
fastifySwagger,
getSwaggerObject([
{ url: "http://localhost:8080", description: "dev" },
{ url: "http://192.168.178.161:20001", description: "prod" },
]),
);
// Swagger UI
const theme = new SwaggerTheme();
const content = theme.getBuffer(SwaggerThemeNameEnum.ONE_DARK);
function getSwaggerUiObject(indexPrefix: string): FastifySwaggerUiOptions {
return {
routePrefix: "/docs",
indexPrefix: indexPrefix,
uiConfig: {
docExpansion: "list",
deepLinking: false,
},
theme: {
css: [{ filename: "theme.css", content: content }],
},
};
}
await publicServer.register(fastifySwaggerUi, getSwaggerUiObject("/api"));
await privateServer.register(fastifySwaggerUi, getSwaggerUiObject(""));
// axios
function getAxiosConfig(): FastifyAxiosOptions {
return {
clients: {
grist: {
baseURL: Config.grist.api_url,
headers: {
Authorization: `Bearer ${Config.grist.api_token}`,
},
},
homeassistant: {
baseURL: Config.homeassistant.api_url,
headers: {
Authorization: `Bearer ${Config.homeassistant.api_token}`,
},
},
tidal: {
baseURL: `${Config.tidal.address}`,
},
},
};
}
await privateServer.register(fastifyAxios, getAxiosConfig());
// Websockets
await publicServer.register(fastifyWebsocket);
// Clients and Services
const gadgetbridgeClient = new GadgetbridgeClient(Config.gadgetbridge.db_path);
const gadgetbridgeService = new GadgetbridgeService(gadgetbridgeClient);
const gristClient = new GristClient(privateServer.axios.grist);
const gristService = new GristService(gristClient);
const haClient = new HomeAssistantClient(privateServer.axios.homeassistant);
const haService = new HomeAssistantService(haClient);
const tidalClient = new TidalClient(privateServer.axios.tidal);
const tidalService = new TidalService(tidalClient);
const wsService = new WsService();
const hpService = new HomepageService(
gristService,
haService,
tidalService,
wsService,
);
hpService.scheduleMidnightGristUpdate();
async function verifyAPIKey(
request: FastifyRequest,
reply: FastifyReply,
): Promise<void> {
const apiKey = request.headers["x-api-key"] ?? request.headers.authorization;
if (!apiKey || apiKey !== Config.api_key) {
logError("POST Request with wrong API key received");
return reply.code(401).send({ error: "Invalid API key" });
}
}
// Register routes
await publicServer.register(publicWsRoutes, { wsService, hpService });
await privateServer.register(privateGadgetbridgeRoutes, {
gadgetbridgeService,
});
await privateServer.register(privateGristRoutes, { gristService });
await privateServer.register(privateHomeAssistantRoutes, {
haService,
verifyAPIKey,
});
await privateServer.register(privateTidalRoutes, {
tidalService,
verifyAPIKey,
});
await privateServer.register(privateHomepageRoutes, {
hpService,
gristService,
haService,
verifyAPIKey,
});
// Ping Routes
publicServer.get(
"/ping",
{
schema: {
description: "Health check endpoint",
tags: ["default"],
response: {
200: z.literal("pong"),
},
},
},
async (_request, _reply) => {
return "pong" as const;
},
);
privateServer.get(
"/ping",
{
schema: {
description: "Health check endpoint",
tags: ["default"],
response: {
200: z.literal("pong"),
},
},
},
async (_request, _reply) => {
return "pong" as const;
},
);
const port = parseInt(Config.port, 10);
const publicPort = port + 1;
await publicServer.ready();
await privateServer.ready();
publicServer.listen({ port: publicPort, host: "0.0.0.0" }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});
privateServer.listen({ port: port, host: "0.0.0.0" }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});