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(); const privateServer = Fastify().withTypeProvider(); 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 { 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}`); });