192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
import { App } from "@/pkg/hono/app";
|
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
|
|
import { rootKeyAuth } from "@/pkg/auth/root_key";
|
|
import { eq, schema, sql } from "@/pkg/db";
|
|
import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors";
|
|
import { buildUnkeyQuery } from "@aigxion/rbac";
|
|
|
|
const route = createRoute({
|
|
method: "post",
|
|
path: "/v1/keys.updateRemaining",
|
|
security: [{ bearerAuth: [] }],
|
|
request: {
|
|
body: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: z.object({
|
|
keyId: z.string().openapi({
|
|
description: "The id of the key you want to modify",
|
|
example: "key_123",
|
|
}),
|
|
op: z.enum(["increment", "decrement", "set"]).openapi({
|
|
description: "The operation you want to perform on the remaining count",
|
|
}),
|
|
value: z.number().int().nullable().openapi({
|
|
description: "The value you want to set, add or subtract the remaining count by",
|
|
example: 1,
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "The configuration for an api",
|
|
content: {
|
|
"application/json": {
|
|
schema: z.object({
|
|
remaining: z.number().int().nullable().openapi({
|
|
description:
|
|
"The number of remaining requests for this key after updating it. `null` means unlimited.",
|
|
example: 100,
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
...openApiErrorResponses,
|
|
},
|
|
});
|
|
|
|
export type Route = typeof route;
|
|
export type V1KeysUpdateRemainingRequest = z.infer<
|
|
(typeof route.request.body.content)["application/json"]["schema"]
|
|
>;
|
|
export type V1KeysUpdateRemainingResponse = z.infer<
|
|
(typeof route.responses)[200]["content"]["application/json"]["schema"]
|
|
>;
|
|
|
|
export const registerV1KeysUpdateRemaining = (app: App) =>
|
|
app.openapi(route, async (c) => {
|
|
const req = c.req.valid("json");
|
|
const { cache, db, usageLimiter, analytics } = c.get("services");
|
|
|
|
const key = await db.query.keys.findFirst({
|
|
where: (table, { eq }) => eq(table.id, req.keyId),
|
|
with: {
|
|
keyAuth: {
|
|
with: {
|
|
api: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!key) {
|
|
throw new UnkeyApiError({ code: "NOT_FOUND", message: `key ${req.keyId} not found` });
|
|
}
|
|
const auth = await rootKeyAuth(
|
|
c,
|
|
buildUnkeyQuery(({ or }) =>
|
|
or("*", "api.*.update_key", `api.${key.keyAuth.api.id}.update_key`),
|
|
),
|
|
);
|
|
if (key.workspaceId !== auth.authorizedWorkspaceId) {
|
|
throw new UnkeyApiError({ code: "NOT_FOUND", message: `key ${req.keyId} not found` });
|
|
}
|
|
|
|
const authorizedWorkspaceId = auth.authorizedWorkspaceId;
|
|
const rootKeyId = auth.key.id;
|
|
switch (req.op) {
|
|
case "increment": {
|
|
if (key.remaining === null) {
|
|
throw new UnkeyApiError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
"cannot increment a key with unlimited remaining requests, please 'set' a value instead.",
|
|
});
|
|
}
|
|
if (req.value === null) {
|
|
throw new UnkeyApiError({
|
|
code: "BAD_REQUEST",
|
|
message: "cannot increment a key by null.",
|
|
});
|
|
}
|
|
await db
|
|
.update(schema.keys)
|
|
.set({
|
|
remaining: sql`remaining_requests + ${req.value}`,
|
|
})
|
|
.where(eq(schema.keys.id, req.keyId));
|
|
break;
|
|
}
|
|
case "decrement": {
|
|
if (key.remaining === null) {
|
|
throw new UnkeyApiError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
"cannot decrement a key with unlimited remaining requests, please 'set' a value instead.",
|
|
});
|
|
}
|
|
if (req.value === null) {
|
|
throw new UnkeyApiError({
|
|
code: "BAD_REQUEST",
|
|
message: "cannot decrement a key by null.",
|
|
});
|
|
}
|
|
await db
|
|
.update(schema.keys)
|
|
.set({
|
|
remaining: sql`remaining_requests - ${req.value}`,
|
|
})
|
|
.where(eq(schema.keys.id, req.keyId));
|
|
break;
|
|
}
|
|
case "set": {
|
|
await db
|
|
.update(schema.keys)
|
|
.set({
|
|
remaining: req.value,
|
|
})
|
|
.where(eq(schema.keys.id, req.keyId));
|
|
break;
|
|
}
|
|
}
|
|
|
|
await Promise.all([
|
|
usageLimiter.revalidate({ keyId: key.id }),
|
|
cache.remove(c, "keyByHash", key.hash),
|
|
cache.remove(c, "keyById", key.id),
|
|
]);
|
|
|
|
const keyAfterUpdate = await db.query.keys.findFirst({
|
|
where: (table, { eq }) => eq(table.id, req.keyId),
|
|
});
|
|
if (!keyAfterUpdate) {
|
|
throw new UnkeyApiError({
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
message: "key not found after update, this should not happen",
|
|
});
|
|
}
|
|
await analytics.ingestAuditLogs({
|
|
actor: {
|
|
type: "key",
|
|
id: rootKeyId,
|
|
},
|
|
event: "key.update",
|
|
workspaceId: authorizedWorkspaceId,
|
|
description: `Changed remaining to ${keyAfterUpdate.remaining}`,
|
|
resources: [
|
|
{
|
|
type: "keyAuth",
|
|
id: key.keyAuthId,
|
|
},
|
|
{
|
|
type: "key",
|
|
id: key.id,
|
|
},
|
|
],
|
|
context: {
|
|
location: c.get("location"),
|
|
userAgent: c.get("userAgent"),
|
|
},
|
|
});
|
|
|
|
return c.json({
|
|
remaining: keyAfterUpdate.remaining,
|
|
});
|
|
});
|