feat: migrate API to @hono/zod-openapi and add Scalar API documentation UI

This commit is contained in:
ramvignesh-b
2026-05-11 16:38:05 +05:30
parent c475f46f33
commit d6f13ec332
2 changed files with 136 additions and 17 deletions
+27 -2
View File
@@ -1,4 +1,5 @@
import { Hono } from "hono";
import { OpenAPIHono } from "@hono/zod-openapi";
import { Scalar } from "@scalar/hono-api-reference";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { config } from "./config";
@@ -7,7 +8,31 @@ import { authRoutes } from "./routes/auth";
import { configRoutes } from "./routes/config";
import { dashboardRoutes } from "./routes/dashboard";
const app = new Hono({ strict: false });
const app = new OpenAPIHono({ strict: false });
// OpenAPI specs
app.doc("/doc", {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "toknd — Auth Broker API",
description: "Centralized token management and OAuth2 broker service.",
},
});
app.openAPIRegistry.registerComponent("securitySchemes", "API_KEY", {
type: "http",
scheme: "bearer",
});
// Scalar API
app.get(
"/ui",
Scalar({
theme: "solarized",
url: "/doc",
}),
);
app.use("*", logger());
app.use("*", prettyJSON());
+109 -15
View File
@@ -1,31 +1,122 @@
import { Hono } from "hono";
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { ConfigManager } from "../core/ConfigManager";
import { redis } from "../core/RedisClient";
import { TokenManager } from "../core/TokenManager";
import { authMiddleware } from "../middleware/auth";
import { GenericProvider } from "../providers/GenericProvider";
const apiRoutes = new Hono({ strict: false });
const apiRoutes = new OpenAPIHono();
// Schemas
const StatusResponseSchema = z
.record(
z.string(),
z.object({
accessToken: z.string().nullable(),
refreshToken: z.string().nullable(),
lastUpdated: z.string().nullable(),
}),
)
.openapi("StatusResponse");
const TokenResponseSchema = z
.object({
access_token: z.string(),
})
.openapi("TokenResponse");
const RefreshResponseSchema = z
.object({
success: z.boolean(),
status: z.object({
accessToken: z.string().nullable(),
refreshToken: z.string().nullable(),
lastUpdated: z.string().nullable(),
}),
})
.openapi("RefreshResponse");
const ErrorSchema = z
.object({
error: z.string(),
})
.openapi("Error");
// Routes
const statusRoute = createRoute({
method: "get",
path: "/status",
security: [{ API_KEY: [] }],
responses: {
200: {
content: { "application/json": { schema: StatusResponseSchema } },
description: "Retrieve the status of all configured providers",
},
},
});
const tokenRoute = createRoute({
method: "get",
path: "/token/{provider}",
security: [{ API_KEY: [] }],
request: {
params: z.object({
provider: z.string().openapi({ example: "trakt" }),
}),
},
responses: {
200: {
content: { "application/json": { schema: TokenResponseSchema } },
description: "Retrieve a valid access token for a specific provider",
},
404: {
content: { "application/json": { schema: ErrorSchema } },
description: "Provider not configured or tokens not found",
},
},
});
const refreshRoute = createRoute({
method: "post",
path: "/refresh/{provider}",
security: [{ API_KEY: [] }],
request: {
params: z.object({
provider: z.string().openapi({ example: "trakt" }),
}),
},
responses: {
200: {
content: { "application/json": { schema: RefreshResponseSchema } },
description: "Manually force a token refresh for a specific provider",
},
404: {
content: { "application/json": { schema: ErrorSchema } },
description: "Provider not configured",
},
},
});
// Implementations
apiRoutes.use("*", authMiddleware);
apiRoutes.get("/status", async (c) => {
apiRoutes.openapi(statusRoute, async (c) => {
const configManager = new ConfigManager(redis);
const providers = await configManager.getAllProviders();
const status: Record<string, { accessToken: string | null; refreshToken: string | null }> = {};
const status: z.infer<typeof StatusResponseSchema> = {};
for (const provider of Object.keys(providers)) {
const accessToken = await redis.get(`provider:${provider}:access_token`);
const refreshToken = await redis.get(`provider:${provider}:refresh_token`);
const lastUpdated = await redis.get(`provider:${provider}:last_updated`);
status[provider] = { accessToken, refreshToken, lastUpdated } as any;
status[provider] = { accessToken, refreshToken, lastUpdated };
}
return c.json(status);
return c.json(status, 200);
});
apiRoutes.get("/token/:provider", async (c) => {
const providerName = c.req.param("provider");
apiRoutes.openapi(tokenRoute, async (c) => {
const providerName = c.req.valid("param").provider;
const configManager = new ConfigManager(redis);
const providerConfig = await configManager.getProviderConfig(providerName);
@@ -40,11 +131,11 @@ apiRoutes.get("/token/:provider", async (c) => {
if (!accessToken) {
return c.json({ error: "No tokens found for provider" }, 404);
}
return c.json({ access_token: accessToken });
return c.json({ access_token: accessToken }, 200);
});
apiRoutes.post("/refresh/:provider", async (c) => {
const providerName = c.req.param("provider");
apiRoutes.openapi(refreshRoute, async (c) => {
const providerName = c.req.valid("param").provider;
const configManager = new ConfigManager(redis);
const providerConfig = await configManager.getProviderConfig(providerName);
@@ -60,10 +151,13 @@ apiRoutes.post("/refresh/:provider", async (c) => {
const refreshToken = await redis.get(`provider:${providerName}:refresh_token`);
const lastUpdated = await redis.get(`provider:${providerName}:last_updated`);
return c.json({
success: true,
status: { accessToken, refreshToken, lastUpdated },
});
return c.json(
{
success: true,
status: { accessToken, refreshToken, lastUpdated },
},
200,
);
});
export { apiRoutes };