diff options
Diffstat (limited to 'packages/web/src/server/api')
| -rw-r--r-- | packages/web/src/server/api/root.ts | 7 | ||||
| -rw-r--r-- | packages/web/src/server/api/routers/api-key.ts | 132 | ||||
| -rw-r--r-- | packages/web/src/server/api/routers/memory.ts | 14 | ||||
| -rw-r--r-- | packages/web/src/server/api/trpc.ts | 1 |
4 files changed, 142 insertions, 12 deletions
diff --git a/packages/web/src/server/api/root.ts b/packages/web/src/server/api/root.ts index b0fd7ba..919ba21 100644 --- a/packages/web/src/server/api/root.ts +++ b/packages/web/src/server/api/root.ts @@ -1,13 +1,10 @@ +import { apiKeyRouter } from "~/server/api/routers/api-key"; import { memoryRouter } from "~/server/api/routers/memory"; import { postRouter } from "~/server/api/routers/post"; import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; -/** - * This is the primary router for your server. - * - * All routers added in /api/routers should be manually added here. - */ export const appRouter = createTRPCRouter({ + apiKey: apiKeyRouter, memory: memoryRouter, post: postRouter, }); diff --git a/packages/web/src/server/api/routers/api-key.ts b/packages/web/src/server/api/routers/api-key.ts new file mode 100644 index 0000000..37c26d6 --- /dev/null +++ b/packages/web/src/server/api/routers/api-key.ts @@ -0,0 +1,132 @@ +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 }; + }), +}); diff --git a/packages/web/src/server/api/routers/memory.ts b/packages/web/src/server/api/routers/memory.ts index d1bb849..e6d206b 100644 --- a/packages/web/src/server/api/routers/memory.ts +++ b/packages/web/src/server/api/routers/memory.ts @@ -39,9 +39,9 @@ async function getOrCreateDefaultProject( } export const memoryRouter = createTRPCRouter({ - list: protectedProcedure.query(async ({ ctx }) => { + list: protectedProcedure.query(async ({ ctx: context }) => { const supabaseClient = await createClient(); - const memoryStore = new SupabaseStore(supabaseClient, ctx.user.id); + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); const memories = await memoryStore.list(); return memories; @@ -49,13 +49,13 @@ export const memoryRouter = createTRPCRouter({ create: protectedProcedure .input(z.object({ content: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { + .mutation(async ({ context, input }) => { const supabaseClient = await createClient(); const projectId = await getOrCreateDefaultProject( supabaseClient, - ctx.user.id, + context.user.id, ); - const memoryStore = new SupabaseStore(supabaseClient, ctx.user.id); + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); const memory = await memoryStore.create({ content: input.content, projectId, @@ -66,9 +66,9 @@ export const memoryRouter = createTRPCRouter({ delete: protectedProcedure .input(z.object({ id: z.string() })) - .mutation(async ({ ctx, input }) => { + .mutation(async ({ context, input }) => { const supabaseClient = await createClient(); - const memoryStore = new SupabaseStore(supabaseClient, ctx.user.id); + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); const deleteResult = await memoryStore.delete(input.id); if (!deleteResult.success) { diff --git a/packages/web/src/server/api/trpc.ts b/packages/web/src/server/api/trpc.ts index 56dcb4c..62897a5 100644 --- a/packages/web/src/server/api/trpc.ts +++ b/packages/web/src/server/api/trpc.ts @@ -127,6 +127,7 @@ export const protectedProcedure = trpcInstance.procedure return next({ ctx: { + ...ctx, user: ctx.user, }, }); |