import type { NextRequest } from "next/server"; import { userAgent } from "next/server"; import { createOpenApiFetchHandler } from "trpc-to-openapi"; import { appRouter, createTRPCContext } from "@homarr/api"; import type { Session } from "@homarr/auth"; import { hashPasswordAsync } from "@homarr/auth"; import { createSessionAsync } from "@homarr/auth/server"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { db, eq } from "@homarr/db"; import { apiKeys } from "@homarr/db/schema"; const logger = createLogger({ module: "trpcOpenApiRoute" }); const handlerAsync = async (req: NextRequest) => { const apiKeyHeaderValue = req.headers.get("ApiKey"); const ipAddress = req.headers.get("x-forwarded-for"); const { ua } = userAgent(req); const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue, ipAddress, ua); // Fallback to JSON if no content type is set if (!req.headers.has("Content-Type")) { req.headers.set("Content-Type", "application/json"); } return createOpenApiFetchHandler({ req, endpoint: "/", router: appRouter, createContext: () => createTRPCContext({ session, headers: req.headers }), onError({ error, path, type }) { logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error })); }, }); }; const getSessionOrDefaultFromHeadersAsync = async ( apiKeyHeaderValue: string | null, ipAdress: string | null, userAgent: string, ): Promise => { logger.info( `Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`, ); if (apiKeyHeaderValue === null) { return null; } const [apiKeyId, apiKey] = apiKeyHeaderValue.split("."); if (!apiKeyId || !apiKey) { logger.warn("An attempt to authenticate over API has failed due to invalid API key format", { ipAdress, userAgent, }); return null; } const apiKeyFromDb = await db.query.apiKeys.findFirst({ where: eq(apiKeys.id, apiKeyId), columns: { id: true, apiKey: true, salt: true, }, with: { user: { columns: { id: true, name: true, email: true, emailVerified: true, }, }, }, }); if (!apiKeyFromDb) { logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent }); return null; } const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt); if (apiKeyFromDb.apiKey !== hashedApiKey) { logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent }); return null; } logger.info("Read session from API request and found user", { name: apiKeyFromDb.user.name, id: apiKeyFromDb.user.id, }); return await createSessionAsync(db, apiKeyFromDb.user); }; export { handlerAsync as DELETE, handlerAsync as GET, handlerAsync as PATCH, handlerAsync as POST, handlerAsync as PUT, };