Add MCP API access

This commit is contained in:
2026-06-04 21:33:32 -04:00
parent a13992e387
commit 37eb70be65
10 changed files with 1050 additions and 2 deletions
+122
View File
@@ -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 };
}),
});