feat: migrate API to @hono/zod-openapi and add Scalar API documentation UI
This commit is contained in:
+27
-2
@@ -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
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user