diff --git a/package-lock.json b/package-lock.json index 210e323..806b51b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@types/luxon": "^3.7.1", "@types/node": "^24.10.1", "@types/ws": "^8.18.1", + "openapi-types": "^12.1.3", "tsx": "^4.20.6", "typescript": "^5.9.3" } diff --git a/package.json b/package.json index ad9854e..62db4ed 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/luxon": "^3.7.1", "@types/node": "^24.10.1", "@types/ws": "^8.18.1", + "openapi-types": "^12.1.3", "tsx": "^4.20.6", "typescript": "^5.9.3" } diff --git a/src/grist/routes.ts b/src/grist/private-routes.ts similarity index 94% rename from src/grist/routes.ts rename to src/grist/private-routes.ts index a38c9e4..72da047 100644 --- a/src/grist/routes.ts +++ b/src/grist/private-routes.ts @@ -3,7 +3,7 @@ import type { FastifyInstance } from "fastify"; import { z } from "zod"; import type { GristService } from "./service"; -export async function gristRoutes( +export async function privateGristRoutes( fastify: FastifyInstance, { gristService, diff --git a/src/homeassistant/routes.ts b/src/homeassistant/private-routes.ts similarity index 94% rename from src/homeassistant/routes.ts rename to src/homeassistant/private-routes.ts index 6e14e5b..47ee617 100644 --- a/src/homeassistant/routes.ts +++ b/src/homeassistant/private-routes.ts @@ -1,9 +1,9 @@ import type { HomeAssistantDeskPositionResult } from "@dpu/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; -import type { HomeAssistantService } from "../homeassistant/service.js"; +import type { HomeAssistantService } from "./service.js"; -export async function homeAssistantRoutes( +export async function privateHomeAssistantRoutes( fastify: FastifyInstance, { haService, diff --git a/src/homepage/routes.ts b/src/homepage/private-routes.ts similarity index 81% rename from src/homepage/routes.ts rename to src/homepage/private-routes.ts index b63f1a7..5ebe8ab 100644 --- a/src/homepage/routes.ts +++ b/src/homepage/private-routes.ts @@ -1,5 +1,4 @@ import type { - API_HA_DeskPosition, ComponentUpdate, GristRecord_PersonalGoals, HA_Update, @@ -14,7 +13,7 @@ import type { GristService } from "../grist/service.js"; import type { HomeAssistantService } from "../homeassistant/service.js"; import type { HomepageService } from "./service.js"; -export async function homepageRoutes( +export async function privateHomepageRoutes( fastify: FastifyInstance, { hpService, @@ -31,39 +30,6 @@ export async function homepageRoutes( ) => Promise; }, ) { - fastify.get( - "/homepage/status", - { - schema: { - description: "Get all current information for dpu status page", - tags: ["homepage"], - response: { - 200: z.object({ - ha_desk_position: z.custom().nullable(), - ha_temp: z.string().nullable(), - tidal_current: z.custom().nullable(), - grist_personal_goals: z - .custom() - .nullable(), - }), - 418: z.object({ - error: z.string(), - }), - }, - }, - }, - async (_request, reply) => { - const service_result = await hpService.getFullInformation(); - - if (!service_result.successful) { - reply.code(418); - return { error: service_result.result }; - } - - return service_result.result; - }, - ); - fastify.post( "/homepage/update/grist", { diff --git a/src/homepage/public-routes.ts b/src/homepage/public-routes.ts new file mode 100644 index 0000000..9ff085f --- /dev/null +++ b/src/homepage/public-routes.ts @@ -0,0 +1,50 @@ +import type { + API_HA_DeskPosition, + GristRecord_PersonalGoals, + TidalGetCurrent, +} from "@dpu/shared"; +import type { FastifyInstance } from "fastify"; +import { z } from "zod"; +import type { HomepageService } from "./service.js"; + +export async function publicHomepageRoutes( + fastify: FastifyInstance, + { + hpService, + }: { + hpService: HomepageService; + }, +) { + fastify.get( + "/homepage/status", + { + schema: { + description: "Get all current information for dpu status page", + tags: ["homepage"], + response: { + 200: z.object({ + ha_desk_position: z.custom().nullable(), + ha_temp: z.string().nullable(), + tidal_current: z.custom().nullable(), + grist_personal_goals: z + .custom() + .nullable(), + }), + 418: z.object({ + error: z.string(), + }), + }, + }, + }, + async (_request, reply) => { + const service_result = await hpService.getFullInformation(); + + if (!service_result.successful) { + reply.code(418); + return { error: service_result.result }; + } + + return service_result.result; + }, + ); +} diff --git a/src/index.ts b/src/index.ts index 41157a1..f2664b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,104 +1,149 @@ import { WsService } from "@dpu/shared"; import { logError } from "@dpu/shared/dist/logger.js"; -import fastifyCors from "@fastify/cors"; -import fastifySwagger from "@fastify/swagger"; -import fastifySwaggerUi from "@fastify/swagger-ui"; +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 from "fastify-axios"; +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 { GristClient } from "./grist/client.js"; -import { gristRoutes } from "./grist/routes.js"; +import { privateGristRoutes } from "./grist/private-routes.js"; import { GristService } from "./grist/service.js"; import { HomeAssistantClient } from "./homeassistant/client.js"; -import { homeAssistantRoutes } from "./homeassistant/routes.js"; +import { privateHomeAssistantRoutes } from "./homeassistant/private-routes.js"; import { HomeAssistantService } from "./homeassistant/service.js"; -import { homepageRoutes } from "./homepage/routes.js"; +import { privateHomepageRoutes } from "./homepage/private-routes.js"; +import { publicHomepageRoutes } from "./homepage/public-routes.js"; import { HomepageService } from "./homepage/service.js"; import { TidalClient } from "./tidal/client.js"; -import { tidalRoutes } from "./tidal/routes.js"; +import { privateTidalRoutes } from "./tidal/private-routes.js"; import { TidalService } from "./tidal/service.js"; -import { wsRoutes } from "./websocket/routes.js"; +import { publicWsRoutes } from "./websocket/public-routes.js"; -const fastify = Fastify().withTypeProvider(); +// Initialize with Zod +const publicServer = Fastify().withTypeProvider(); +const privateServer = Fastify().withTypeProvider(); +publicServer.setValidatorCompiler(validatorCompiler); +publicServer.setSerializerCompiler(serializerCompiler); +privateServer.setValidatorCompiler(validatorCompiler); +privateServer.setSerializerCompiler(serializerCompiler); -fastify.setValidatorCompiler(validatorCompiler); -fastify.setSerializerCompiler(serializerCompiler); +// Cors +function getCorsObject(urls: string[]): FastifyCorsOptions { + return { + origin: urls, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + }; +} -await fastify.register(fastifyCors, { - origin: ["http://localhost:4321", "https://dariusbag.dev"], - 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"]), +); -await fastify.register(fastifySwagger, { - openapi: { - info: { - title: "DPU API", - description: "API Documentation", - version: "1.0.0", +// Swagger +function getSwaggerObject(servers: OpenAPIV3.ServerObject[]): SwaggerOptions { + return { + openapi: { + info: { + title: "DPU API", + description: "API Documentation", + version: "1.0.0", + }, + servers: servers, }, - servers: [ - { url: "http://localhost:8080", description: "dev" }, - { url: "https://dpu.dariusbag.dev/api", description: "prod" }, - ], - }, - transform: jsonSchemaTransform, -}); + transform: jsonSchemaTransform, + }; +} +await publicServer.register( + fastifySwagger, + getSwaggerObject([ + { url: "https://dpu.dariusbag.dev/api", description: "prod" }, + ]), +); +await privateServer.register( + fastifySwagger, + getSwaggerObject([ + { url: "http://192.168.178.161:20001", description: "prod" }, + ]), +); + +// Swagger UI const theme = new SwaggerTheme(); const content = theme.getBuffer(SwaggerThemeNameEnum.ONE_DARK); -await fastify.register(fastifySwaggerUi, { - routePrefix: "/docs", - indexPrefix: `${Config.env_dev ? "" : "/api"}`, - uiConfig: { - docExpansion: "list", - deepLinking: false, - }, - theme: { - css: [{ filename: "theme.css", content: content }], - }, -}); +function getSwaggerUiObject(indexPrefix: string): FastifySwaggerUiOptions { + return { + routePrefix: "/docs", + indexPrefix: indexPrefix, + uiConfig: { + docExpansion: "list", + deepLinking: false, + }, + theme: { + css: [{ filename: "theme.css", content: content }], + }, + }; +} -await fastify.register(fastifyAxios, { - clients: { - grist: { - baseURL: Config.grist.api_url, - headers: { - Authorization: `Bearer ${Config.grist.api_token}`, +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}`, }, }, - homeassistant: { - baseURL: Config.homeassistant.api_url, - headers: { - Authorization: `Bearer ${Config.homeassistant.api_token}`, - }, - }, - tidal: { - baseURL: `${Config.tidal.address}`, - }, - }, -}); + }; +} -await fastify.register(fastifyWebsocket); +await privateServer.register(fastifyAxios, getAxiosConfig()); -const gristClient = new GristClient(fastify.axios.grist); +// Websockets +await publicServer.register(fastifyWebsocket); + +// Clients and Services +const gristClient = new GristClient(privateServer.axios.grist); const gristService = new GristService(gristClient); -const haClient = new HomeAssistantClient(fastify.axios.homeassistant); +const haClient = new HomeAssistantClient(privateServer.axios.homeassistant); const haService = new HomeAssistantService(haClient); -const tidalClient = new TidalClient(fastify.axios.tidal); +const tidalClient = new TidalClient(privateServer.axios.tidal); const tidalService = new TidalService(tidalClient); const wsService = new WsService(); @@ -122,21 +167,45 @@ async function verifyAPIKey( } } -const port = parseInt(Config.port, 10); - // Register routes -await fastify.register(gristRoutes, { gristService }); -await fastify.register(homeAssistantRoutes, { haService, verifyAPIKey }); -await fastify.register(tidalRoutes, { tidalService, verifyAPIKey }); -await fastify.register(wsRoutes, { wsService }); -await fastify.register(homepageRoutes, { +await publicServer.register(publicWsRoutes, { wsService }); +await publicServer.register(publicHomepageRoutes, { + hpService, +}); + +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, }); -fastify.get( +// 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: { @@ -152,8 +221,20 @@ fastify.get( }, ); -await fastify.ready(); -fastify.listen({ port: port, host: "0.0.0.0" }, (err, address) => { +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); diff --git a/src/tidal/routes.ts b/src/tidal/private-routes.ts similarity index 96% rename from src/tidal/routes.ts rename to src/tidal/private-routes.ts index 2a8b436..b0ca9e7 100644 --- a/src/tidal/routes.ts +++ b/src/tidal/private-routes.ts @@ -1,8 +1,8 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; -import type { TidalService } from "../tidal/service.js"; +import type { TidalService } from "./service.js"; -export async function tidalRoutes( +export async function privateTidalRoutes( fastify: FastifyInstance, { tidalService, diff --git a/src/websocket/routes.ts b/src/websocket/public-routes.ts similarity index 94% rename from src/websocket/routes.ts rename to src/websocket/public-routes.ts index 9b791da..050a68c 100644 --- a/src/websocket/routes.ts +++ b/src/websocket/public-routes.ts @@ -1,7 +1,7 @@ import { logInfo, type WsService } from "@dpu/shared"; import type { FastifyInstance } from "fastify"; -export async function wsRoutes( +export async function publicWsRoutes( fastify: FastifyInstance, { wsService,