hts/apps/api/src/routes/legacy_apis_listKeys.ts

149 lines
4.6 KiB
TypeScript

import { App } from "@/pkg/hono/app";
import { createRoute, z } from "@hono/zod-openapi";
import { and, eq, isNull, sql } from "drizzle-orm";
import { rootKeyAuth } from "@/pkg/auth/root_key";
import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors";
import { schema } from "@aigxion/db";
import { keySchema } from "./schema";
const route = createRoute({
method: "get",
path: "/v1/apis/{apiId}/keys",
request: {
header: z.object({
authorization: z
.string()
.regex(/^Bearer [a-zA-Z0-9_]+/)
.openapi({
description: "A root key to authorize the request formatted as bearer token",
example: "Bearer unkey_1234",
}),
}),
params: z.object({
apiId: z.string().min(1).openapi({
description: "The id of the api to fetch",
example: "api_1234",
}),
}),
query: z.object({
limit: z.coerce.number().int().min(1).max(100).optional().default(100).openapi({
description: "The maximum number of keys to return",
example: 100,
}),
offset: z.coerce.number().optional().openapi({
description:
"Use this to fetch the next page of results. A new cursor will be returned in the response if there are more results.",
}),
ownerId: z.string().min(1).optional().openapi({
description: "If provided, this will only return keys where the `ownerId` matches.",
}),
}),
},
responses: {
200: {
description: "Keys belonging to the api",
content: {
"application/json": {
schema: z.object({
keys: z.array(keySchema),
total: z.number().int().openapi({
description: "The total number of keys for this api",
}),
}),
},
},
},
...openApiErrorResponses,
},
});
export type Route = typeof route;
export type LegacyApisListKeysResponse = z.infer<
(typeof route.responses)[200]["content"]["application/json"]["schema"]
>;
export const registerLegacyApisListKeys = (app: App) =>
app.openapi(route, async (c) => {
const { db, cache } = c.get("services");
const auth = await rootKeyAuth(c);
const apiId = c.req.param("apiId");
const { limit, offset, ownerId } = c.req.query();
const { val: api, err } = await cache.withCache(c, "apiById", apiId, async () => {
return (
(await db.query.apis.findFirst({
where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)),
})) ?? null
);
});
if (err) {
throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: `unable to load api: ${err.message}`,
});
}
if (!api || api.workspaceId !== auth.authorizedWorkspaceId) {
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `api ${apiId} not found`,
});
}
if (!api.keyAuthId) {
throw new UnkeyApiError({
code: "PRECONDITION_FAILED",
message: `api ${apiId} is not setup to handle keys`,
});
}
const keysWhere: Parameters<typeof and> = [
isNull(schema.keys.deletedAt),
eq(schema.keys.keyAuthId, api.keyAuthId),
];
if (ownerId) {
keysWhere.push(eq(schema.keys.ownerId, ownerId));
}
const keys = await db.query.keys.findMany({
where: and(...keysWhere),
limit: parseInt(limit),
orderBy: schema.keys.id,
offset: offset ? parseInt(offset) : undefined,
});
const total = await db
// @ts-ignore, mysql sucks
.select({ count: sql<string>`count(*)` })
.from(schema.keys)
.where(and(eq(schema.keys.keyAuthId, api.keyAuthId), isNull(schema.keys.deletedAt)));
return c.json({
keys: keys.map((k) => ({
id: k.id,
start: k.start,
apiId: api.id,
workspaceId: k.workspaceId,
name: k.name ?? undefined,
ownerId: k.ownerId ?? undefined,
meta: k.meta ? JSON.parse(k.meta) : undefined,
createdAt: k.createdAt.getTime() ?? undefined,
expires: k.expires?.getTime() ?? undefined,
ratelimit:
k.ratelimitType && k.ratelimitLimit && k.ratelimitRefillRate && k.ratelimitRefillInterval
? {
type: k.ratelimitType,
limit: k.ratelimitLimit,
refillRate: k.ratelimitRefillRate,
refillInterval: k.ratelimitRefillInterval,
}
: undefined,
remaining: k.remaining ?? undefined,
})),
// @ts-ignore, mysql sucks
total: parseInt(total.at(0)?.count ?? "0"),
cursor: keys.at(-1)?.id ?? undefined,
});
});