import { extractKeyPrefix, generateApiKey, hashApiKey } from "@imemio/sdk"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { createClient } from "~/lib/supabase/server"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; export const apiKeyRouter = createTRPCRouter({ list: protectedProcedure.query(async ({ ctx: context }) => { const supabase = await createClient(); const { data, error } = await supabase .from("api_keys") .select( "id, name, key_prefix, last_used_at, expires_at, revoked_at, created_at", ) .eq("user_id", context.user.id) .is("revoked_at", null) .order("created_at", { ascending: false }); if (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch API keys", }); } return data.map((key) => ({ id: key.id as string, name: key.name as string, keyPrefix: key.key_prefix as string, lastUsedAt: key.last_used_at ? new Date(key.last_used_at as string) : null, expiresAt: key.expires_at ? new Date(key.expires_at as string) : null, createdAt: new Date(key.created_at as string), })); }), create: protectedProcedure .input( z.object({ name: z.string().min(1).max(100), expiresAt: z.date().optional(), }), ) .mutation(async ({ ctx: context, input }) => { const supabase = await createClient(); const apiKey = generateApiKey(); const keyPrefix = extractKeyPrefix(apiKey); const keyHash = await hashApiKey(apiKey); const { data, error } = await supabase .from("api_keys") .insert({ user_id: context.user.id, name: input.name, key_prefix: keyPrefix, key_hash: keyHash, expires_at: input.expiresAt?.toISOString() ?? null, }) .select("id, name, key_prefix, created_at") .single(); if (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create API key", }); } return { id: data.id as string, name: data.name as string, keyPrefix: data.key_prefix as string, createdAt: new Date(data.created_at as string), apiKey, }; }), revoke: protectedProcedure .input(z.object({ id: z.string().uuid() })) .mutation(async ({ ctx: context, input }) => { const supabase = await createClient(); const { error, count } = await supabase .from("api_keys") .update({ revoked_at: new Date().toISOString() }) .eq("id", input.id) .eq("user_id", context.user.id) .is("revoked_at", null); if (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to revoke API key", }); } if (count === 0) { throw new TRPCError({ code: "NOT_FOUND", message: "API key not found", }); } return { success: true }; }), delete: protectedProcedure .input(z.object({ id: z.string().uuid() })) .mutation(async ({ ctx: context, input }) => { const supabase = await createClient(); const { error, count } = await supabase .from("api_keys") .delete({ count: "exact" }) .eq("id", input.id) .eq("user_id", context.user.id); if (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to delete API key", }); } if (count === 0) { throw new TRPCError({ code: "NOT_FOUND", message: "API key not found", }); } return { success: true }; }), });