Add MCP API access
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createApiKeySecret,
|
||||
getApiKeyDisplayPrefix,
|
||||
hashApiKey,
|
||||
} from "~/server/api/api-keys";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
import { apiKeys } from "~/server/db/schema";
|
||||
|
||||
function requireSessionAuth(ctx: { authSource: "session" | "api-key" | "none" }) {
|
||||
if (ctx.authSource !== "session") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "API keys can only be managed from an authenticated session",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const apiKeysRouter = createTRPCRouter({
|
||||
list: protectedProcedure.query(async ({ ctx }) => {
|
||||
requireSessionAuth(ctx);
|
||||
|
||||
return ctx.db.query.apiKeys.findMany({
|
||||
where: eq(apiKeys.userId, ctx.session.user.id),
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
keyPrefix: true,
|
||||
lastUsedAt: true,
|
||||
expiresAt: true,
|
||||
revokedAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
orderBy: (apiKeys, { desc }) => [desc(apiKeys.createdAt)],
|
||||
});
|
||||
}),
|
||||
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().trim().min(1).max(100),
|
||||
expiresAt: z.date().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requireSessionAuth(ctx);
|
||||
|
||||
if (input.expiresAt && input.expiresAt <= new Date()) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Expiration must be in the future",
|
||||
});
|
||||
}
|
||||
|
||||
const key = createApiKeySecret();
|
||||
const [apiKey] = await ctx.db
|
||||
.insert(apiKeys)
|
||||
.values({
|
||||
name: input.name,
|
||||
keyHash: hashApiKey(key),
|
||||
keyPrefix: getApiKeyDisplayPrefix(key),
|
||||
userId: ctx.session.user.id,
|
||||
expiresAt: input.expiresAt ?? null,
|
||||
})
|
||||
.returning({
|
||||
id: apiKeys.id,
|
||||
name: apiKeys.name,
|
||||
keyPrefix: apiKeys.keyPrefix,
|
||||
expiresAt: apiKeys.expiresAt,
|
||||
createdAt: apiKeys.createdAt,
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to create API key",
|
||||
});
|
||||
}
|
||||
|
||||
return { ...apiKey, key };
|
||||
}),
|
||||
|
||||
revoke: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requireSessionAuth(ctx);
|
||||
|
||||
const now = new Date();
|
||||
const [apiKey] = await ctx.db
|
||||
.update(apiKeys)
|
||||
.set({ revokedAt: now, updatedAt: now })
|
||||
.where(
|
||||
and(eq(apiKeys.id, input.id), eq(apiKeys.userId, ctx.session.user.id)),
|
||||
)
|
||||
.returning({ id: apiKeys.id });
|
||||
|
||||
if (!apiKey) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "API key not found",
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
revokeAll: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
requireSessionAuth(ctx);
|
||||
|
||||
const now = new Date();
|
||||
await ctx.db
|
||||
.update(apiKeys)
|
||||
.set({ revokedAt: now, updatedAt: now })
|
||||
.where(eq(apiKeys.userId, ctx.session.user.id));
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user