aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/server
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-12 14:52:00 +0000
committerMohamedBassem <me@mbassem.com>2024-02-12 14:55:00 +0000
commit6aacc0c7a86e36c52a3c2c1d26fe58cefcd3bec4 (patch)
treebad306e872d6bfcc2c67f00caa3880c8aa56070f /packages/web/server
parent230cafb6dfc8d3bad57d84ef13c3669f5bf5331a (diff)
downloadkarakeep-6aacc0c7a86e36c52a3c2c1d26fe58cefcd3bec4.tar.zst
feature: Add support for managing API keys
Diffstat (limited to 'packages/web/server')
-rw-r--r--packages/web/server/api/routers/_app.ts2
-rw-r--r--packages/web/server/api/routers/apiKeys.ts67
-rw-r--r--packages/web/server/auth.ts75
3 files changed, 144 insertions, 0 deletions
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,
+ };
+}