123 lines
3.1 KiB
TypeScript
123 lines
3.1 KiB
TypeScript
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 };
|
|
}),
|
|
});
|