From 6aacc0c7a86e36c52a3c2c1d26fe58cefcd3bec4 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Mon, 12 Feb 2024 14:52:00 +0000 Subject: feature: Add support for managing API keys --- packages/web/server/api/routers/_app.ts | 2 + packages/web/server/api/routers/apiKeys.ts | 67 ++++++++++++++++++++++++++ packages/web/server/auth.ts | 75 ++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 packages/web/server/api/routers/apiKeys.ts (limited to 'packages/web/server') diff --git a/packages/web/server/api/routers/_app.ts b/packages/web/server/api/routers/_app.ts index a4f9c629..2097b47d 100644 --- a/packages/web/server/api/routers/_app.ts +++ b/packages/web/server/api/routers/_app.ts @@ -1,7 +1,9 @@ import { router } from "../trpc"; +import { apiKeysAppRouter } from "./apiKeys"; import { bookmarksAppRouter } from "./bookmarks"; export const appRouter = router({ bookmarks: bookmarksAppRouter, + apiKeys: apiKeysAppRouter, }); // export type definition of API export type AppRouter = typeof appRouter; diff --git a/packages/web/server/api/routers/apiKeys.ts b/packages/web/server/api/routers/apiKeys.ts new file mode 100644 index 00000000..b681d43f --- /dev/null +++ b/packages/web/server/api/routers/apiKeys.ts @@ -0,0 +1,67 @@ +import { generateApiKey } from "@/server/auth"; +import { authedProcedure, router } from "../trpc"; +import { prisma } from "@remember/db"; +import { z } from "zod"; + +export const apiKeysAppRouter = router({ + create: authedProcedure + .input( + z.object({ + name: z.string(), + }), + ) + .output( + z.object({ + id: z.string(), + name: z.string(), + key: z.string(), + createdAt: z.date(), + }), + ) + .mutation(async ({ input, ctx }) => { + return await generateApiKey(input.name, ctx.user.id); + }), + revoke: authedProcedure + .input( + z.object({ + id: z.string(), + }), + ) + .output(z.object({})) + .mutation(async ({ input, ctx }) => { + const resp = await prisma.apiKey.delete({ + where: { + id: input.id, + userId: ctx.user.id, + }, + }); + return resp; + }), + list: authedProcedure + .output( + z.object({ + keys: z.array( + z.object({ + id: z.string(), + name: z.string(), + createdAt: z.date(), + keyId: z.string(), + }), + ), + }), + ) + .query(async ({ ctx }) => { + const resp = await prisma.apiKey.findMany({ + where: { + userId: ctx.user.id, + }, + select: { + id: true, + name: true, + createdAt: true, + keyId: true, + }, + }); + return { keys: resp }; + }), +}); diff --git a/packages/web/server/auth.ts b/packages/web/server/auth.ts index 05d3d296..f78fa8cf 100644 --- a/packages/web/server/auth.ts +++ b/packages/web/server/auth.ts @@ -4,6 +4,9 @@ import AuthentikProvider from "next-auth/providers/authentik"; import serverConfig from "@/server/config"; import { prisma } from "@remember/db"; import { DefaultSession } from "next-auth"; +import * as bcrypt from "bcrypt"; + +import { randomBytes } from "crypto"; declare module "next-auth" { /** @@ -37,3 +40,75 @@ export const authOptions: NextAuthOptions = { export const authHandler = NextAuth(authOptions); export const getServerAuthSession = () => getServerSession(authOptions); + +// API Keys + +const BCRYPT_SALT_ROUNDS = 10; +const API_KEY_PREFIX = "ak1"; + +export async function generateApiKey(name: string, userId: string) { + const id = randomBytes(10).toString("hex"); + const secret = randomBytes(10).toString("hex"); + const secretHash = await bcrypt.hash(secret, BCRYPT_SALT_ROUNDS); + + const plain = `${API_KEY_PREFIX}_${id}_${secret}`; + + const key = await prisma.apiKey.create({ + data: { + name: name, + userId: userId, + keyId: id, + keyHash: secretHash, + }, + }); + + return { + id: key.id, + name: key.name, + createdAt: key.createdAt, + key: plain, + }; +} + +function parseApiKey(plain: string) { + const parts = plain.split("_"); + if (parts.length != 3) { + throw new Error( + `Malformd API key. API keys should have 3 segments, found ${parts.length} instead.`, + ); + } + if (parts[0] !== API_KEY_PREFIX) { + throw new Error(`Malformd API key. Got unexpected key prefix.`); + } + return { + keyId: parts[1], + keySecret: parts[2], + }; +} + +export async function authenticateApiKey(key: string) { + const { keyId, keySecret } = parseApiKey(key); + const apiKey = await prisma.apiKey.findUnique({ + where: { + keyId, + }, + include: { + user: true, + }, + }); + + if (!apiKey) { + throw new Error("API key not found"); + } + + const hash = apiKey.keyHash; + + const validation = await bcrypt.compare(keySecret, hash); + if (!validation) { + throw new Error("Invalid API Key"); + } + + return { + user: apiKey.user, + }; +} -- cgit v1.2.3-70-g09d2