diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..6cfd699 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,6 @@ +import { API_VERSION, APP_VERSION } from "./version"; + +export { API_VERSION, APP_VERSION }; +export const API_PREFIX = `/api/${API_VERSION}`; +export const AUTH_PREFIX = `/${API_VERSION}/auth`; +export const DOCS_PREFIX = `/docs/${API_VERSION}`; diff --git a/src/core/ConfigManager.ts b/src/core/ConfigManager.ts index 2d9a350..e3a7cbd 100644 --- a/src/core/ConfigManager.ts +++ b/src/core/ConfigManager.ts @@ -40,4 +40,13 @@ export class ConfigManager { return result; } + + async deleteProviderConfig(provider: string): Promise { + await this.redis.del(`config:${provider}`); + // Also clean up tokens + const tokenKeys = await this.redis.keys(`provider:${provider}:*`); + if (tokenKeys.length > 0) { + await this.redis.del(...tokenKeys); + } + } } diff --git a/src/index.ts b/src/index.ts index d88247e..4939315 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,9 @@ import { serveStatic } from "hono/bun"; import { logger } from "hono/logger"; import { prettyJSON } from "hono/pretty-json"; import { config } from "./config"; +import { API_PREFIX, AUTH_PREFIX, DOCS_PREFIX } from "./constants"; import { redis } from "./core/RedisClient"; +import { openApiSpec, securityScheme } from "./openapi"; import { apiRoutes } from "./routes/api"; import { authRoutes } from "./routes/auth"; import { configRoutes } from "./routes/config"; @@ -13,29 +15,19 @@ import { dashboardRoutes } from "./routes/dashboard"; 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", -}); +app.doc(`${DOCS_PREFIX}/openapi.json`, openApiSpec); +app.openAPIRegistry.registerComponent("securitySchemes", "API_KEY", securityScheme); // Scalar API Reference app.get( - "/api", + DOCS_PREFIX, Scalar({ theme: "solarized", - url: "/doc", + url: `${DOCS_PREFIX}/openapi.json`, }), ); -app.get("/docs", (c) => c.redirect("/api")); +app.get("/docs", (c) => c.redirect(DOCS_PREFIX)); +app.get("/api", (c) => c.redirect(DOCS_PREFIX)); app.use("*", logger()); app.use("*", prettyJSON()); @@ -43,9 +35,9 @@ app.use("*", prettyJSON()); app.get("/", (c) => c.redirect("/app")); app.get("/app/dashboard.js", serveStatic({ path: "./src/views/dashboard.js" })); -app.route("/auth", authRoutes); -app.route("/api/config", configRoutes); -app.route("/api", apiRoutes); +app.route(AUTH_PREFIX, authRoutes); +app.route(`${API_PREFIX}/config`, configRoutes); +app.route(API_PREFIX, apiRoutes); app.route("/app", dashboardRoutes); app.notFound((c) => { diff --git a/src/openapi.ts b/src/openapi.ts new file mode 100644 index 0000000..dbfb7d5 --- /dev/null +++ b/src/openapi.ts @@ -0,0 +1,31 @@ +import { API_VERSION, APP_VERSION } from "./constants"; + +export const openApiSpec = { + openapi: "3.0.0", + info: { + version: `${API_VERSION}.${APP_VERSION}`, + title: "toknd Auth Broker API", + description: + "A high-performance OAuth2 broker and token management service. Designed to centralize provider configurations and automate token lifecycle management across distributed systems.", + }, + tags: [ + { + name: "Tokens", + description: "Endpoint operations for accessing and force-refreshing active provider tokens.", + }, + { + name: "Management", + description: "Administrative operations for provider lifecycle and configuration.", + }, + { + name: "Auth (Internal)", + description: "System-level OAuth2 handshake and callback processing.", + }, + ], + security: [{ API_KEY: [] }], +}; + +export const securityScheme = { + type: "http", + scheme: "bearer", +} as const; diff --git a/src/routes/api.ts b/src/routes/api.ts index 3aa28dd..43915bf 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -47,6 +47,7 @@ const statusRoute = createRoute({ method: "get", path: "/status", security: [{ API_KEY: [] }], + tags: ["Tokens"], responses: { 200: { content: { "application/json": { schema: StatusResponseSchema } }, @@ -59,6 +60,7 @@ const tokenRoute = createRoute({ method: "get", path: "/token/{provider}", security: [{ API_KEY: [] }], + tags: ["Tokens"], request: { params: z.object({ provider: z.string().openapi({ example: "trakt" }), @@ -80,6 +82,7 @@ const refreshRoute = createRoute({ method: "post", path: "/refresh/{provider}", security: [{ API_KEY: [] }], + tags: ["Tokens"], request: { params: z.object({ provider: z.string().openapi({ example: "trakt" }), diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 0b3d3b0..8caf72d 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -17,6 +17,8 @@ const AuthErrorResponse = z const loginRoute = createRoute({ method: "get", path: "/{provider}/login", + tags: ["Auth (Internal)"], + summary: "Start OAuth2 flow (Managed by System)", request: { params: z.object({ provider: z.string().openapi({ example: "trakt" }), @@ -36,6 +38,8 @@ const loginRoute = createRoute({ const callbackRoute = createRoute({ method: "get", path: "/callback", + tags: ["Auth (Internal)"], + summary: "OAuth2 callback handler (Managed by System)", request: { query: z.object({ state: z diff --git a/src/routes/config.ts b/src/routes/config.ts index d8d3b6d..0da7bf3 100644 --- a/src/routes/config.ts +++ b/src/routes/config.ts @@ -38,6 +38,7 @@ const listConfigRoute = createRoute({ method: "get", path: "/", security: [{ API_KEY: [] }], + tags: ["Management"], responses: { 200: { content: { "application/json": { schema: AllProvidersResponse } }, @@ -50,6 +51,7 @@ const setConfigRoute = createRoute({ method: "post", path: "/{provider}", security: [{ API_KEY: [] }], + tags: ["Management"], request: { params: z.object({ provider: z.string().openapi({ example: "trakt" }), @@ -70,6 +72,24 @@ const setConfigRoute = createRoute({ }, }); +const deleteConfigRoute = createRoute({ + method: "delete", + path: "/{provider}", + security: [{ API_KEY: [] }], + tags: ["Management"], + request: { + params: z.object({ + provider: z.string().openapi({ example: "trakt" }), + }), + }, + responses: { + 200: { + content: { "application/json": { schema: SuccessMessage } }, + description: "Delete a provider configuration and its tokens", + }, + }, +}); + // Implementations configRoutes.use("*", authMiddleware); @@ -88,4 +108,12 @@ configRoutes.openapi(setConfigRoute, async (c) => { return c.json({ message: `Config for ${provider} saved successfully` }, 200); }); +configRoutes.openapi(deleteConfigRoute, async (c) => { + const provider = c.req.valid("param").provider; + const configManager = new ConfigManager(redis); + + await configManager.deleteProviderConfig(provider); + return c.json({ message: `Config for ${provider} deleted successfully` }, 200); +}); + export { configRoutes }; diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index aa5bca3..52715f0 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -4,6 +4,7 @@ import { deleteCookie, getCookie, setCookie } from "hono/cookie"; import { html } from "hono/html"; import type { Child } from "hono/jsx"; import { config } from "../config"; +import { API_PREFIX, API_VERSION, APP_VERSION, AUTH_PREFIX, DOCS_PREFIX } from "../constants"; const dashboardRoutes = new Hono({ strict: false }); @@ -39,7 +40,14 @@ export const Layout = (props: { title: string; children: Child; isUnlocked?: boo {props.children} @@ -52,7 +60,7 @@ export const Layout = (props: { title: string; children: Child; isUnlocked?: boo export const Dashboard = (props: { isUnlocked: boolean }) => (