aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-05 13:11:06 +0000
committerMohamedBassem <me@mbassem.com>2024-03-05 13:11:06 +0000
commit8a46ecb7373d6c5e7300861169ea51a7917cd2b4 (patch)
tree4ad318c3b5fc8b7a74cba6d0e37b6ade24db829a /packages/trpc
parent224aa38d5976523f213e2860b6addc7630d472ba (diff)
downloadkarakeep-8a46ecb7373d6c5e7300861169ea51a7917cd2b4.tar.zst
refactor: Extract trpc logic into its package
Diffstat (limited to '')
-rw-r--r--packages/trpc/auth.ts99
-rw-r--r--packages/trpc/index.ts (renamed from packages/web/server/api/trpc.ts)6
-rw-r--r--packages/trpc/package.json28
-rw-r--r--packages/trpc/routers/_app.ts (renamed from packages/web/server/api/routers/_app.ts)2
-rw-r--r--packages/trpc/routers/admin.ts (renamed from packages/web/server/api/routers/admin.ts)2
-rw-r--r--packages/trpc/routers/apiKeys.ts (renamed from packages/web/server/api/routers/apiKeys.ts)4
-rw-r--r--packages/trpc/routers/bookmarks.test.ts (renamed from packages/web/server/api/routers/bookmarks.test.ts)2
-rw-r--r--packages/trpc/routers/bookmarks.ts (renamed from packages/web/server/api/routers/bookmarks.ts)6
-rw-r--r--packages/trpc/routers/lists.ts (renamed from packages/web/server/api/routers/lists.ts)4
-rw-r--r--packages/trpc/routers/users.test.ts (renamed from packages/web/server/api/routers/users.test.ts)2
-rw-r--r--packages/trpc/routers/users.ts (renamed from packages/web/server/api/routers/users.ts)6
-rw-r--r--packages/trpc/testUtils.ts (renamed from packages/web/lib/testUtils.ts)4
-rw-r--r--packages/trpc/tsconfig.json13
-rw-r--r--packages/trpc/types/bookmarks.ts (renamed from packages/web/lib/types/api/bookmarks.ts)2
-rw-r--r--packages/trpc/types/lists.ts (renamed from packages/web/lib/types/api/lists.ts)0
-rw-r--r--packages/trpc/types/tags.ts (renamed from packages/web/lib/types/api/tags.ts)0
-rw-r--r--packages/trpc/types/users.ts (renamed from packages/web/lib/types/api/users.ts)0
-rw-r--r--packages/trpc/vitest.config.ts14
18 files changed, 176 insertions, 18 deletions
diff --git a/packages/trpc/auth.ts b/packages/trpc/auth.ts
new file mode 100644
index 00000000..6854303b
--- /dev/null
+++ b/packages/trpc/auth.ts
@@ -0,0 +1,99 @@
+import { randomBytes } from "crypto";
+import { apiKeys } from "@hoarder/db/schema";
+import * as bcrypt from "bcrypt";
+import { db } from "@hoarder/db";
+
+// 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 db
+ .insert(apiKeys)
+ .values({
+ name: name,
+ userId: userId,
+ keyId: id,
+ keyHash: secretHash,
+ })
+ .returning()
+ )[0];
+
+ 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 db.query.apiKeys.findFirst({
+ where: (k, { eq }) => eq(k.keyId, keyId),
+ with: {
+ 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 apiKey.user;
+}
+
+export async function hashPassword(password: string) {
+ return bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
+}
+
+export async function validatePassword(email: string, password: string) {
+ const user = await db.query.users.findFirst({
+ where: (u, { eq }) => eq(u.email, email),
+ });
+
+ if (!user) {
+ throw new Error("User not found");
+ }
+
+ if (!user.password) {
+ throw new Error("This user doesn't have a password defined");
+ }
+
+ const validation = await bcrypt.compare(password, user.password);
+ if (!validation) {
+ throw new Error("Wrong password");
+ }
+
+ return user;
+}
diff --git a/packages/web/server/api/trpc.ts b/packages/trpc/index.ts
index 0ba09e94..a32eb871 100644
--- a/packages/web/server/api/trpc.ts
+++ b/packages/trpc/index.ts
@@ -1,9 +1,13 @@
import { db } from "@hoarder/db";
import serverConfig from "@hoarder/shared/config";
import { TRPCError, initTRPC } from "@trpc/server";
-import { User } from "next-auth";
import superjson from "superjson";
+type User = {
+ id: string;
+ role: "admin" | "user" | null;
+};
+
export type Context = {
user: User | null;
db: typeof db;
diff --git a/packages/trpc/package.json b/packages/trpc/package.json
new file mode 100644
index 00000000..1e33eff0
--- /dev/null
+++ b/packages/trpc/package.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@hoarder/trpc",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "typecheck": "tsc --noEmit",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "@hoarder/db": "workspace:*",
+ "@hoarder/shared": "workspace:*",
+ "@trpc/server": "11.0.0-next-beta.304",
+ "bcrypt": "^5.1.1",
+ "drizzle-orm": "^0.29.4",
+ "superjson": "^2.2.1",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@tsconfig/node21": "^21.0.1",
+ "@types/bcrypt": "^5.0.2",
+ "aws-sdk": "^2.1570.0",
+ "mock-aws-s3": "^4.0.2",
+ "nock": "^13.5.4",
+ "vite-tsconfig-paths": "^4.3.1",
+ "vitest": "^1.3.1"
+ }
+}
diff --git a/packages/web/server/api/routers/_app.ts b/packages/trpc/routers/_app.ts
index 43ab6f5d..6e5dd91d 100644
--- a/packages/web/server/api/routers/_app.ts
+++ b/packages/trpc/routers/_app.ts
@@ -1,4 +1,4 @@
-import { router } from "../trpc";
+import { router } from "../index";
import { adminAppRouter } from "./admin";
import { apiKeysAppRouter } from "./apiKeys";
import { bookmarksAppRouter } from "./bookmarks";
diff --git a/packages/web/server/api/routers/admin.ts b/packages/trpc/routers/admin.ts
index c3f6235a..8a7b592d 100644
--- a/packages/web/server/api/routers/admin.ts
+++ b/packages/trpc/routers/admin.ts
@@ -1,4 +1,4 @@
-import { adminProcedure, router } from "../trpc";
+import { adminProcedure, router } from "../index";
import { z } from "zod";
import { count } from "drizzle-orm";
import { bookmarks, users } from "@hoarder/db/schema";
diff --git a/packages/web/server/api/routers/apiKeys.ts b/packages/trpc/routers/apiKeys.ts
index 9eb36974..d13f87fb 100644
--- a/packages/web/server/api/routers/apiKeys.ts
+++ b/packages/trpc/routers/apiKeys.ts
@@ -1,5 +1,5 @@
-import { generateApiKey } from "@/server/auth";
-import { authedProcedure, router } from "../trpc";
+import { generateApiKey } from "../auth";
+import { authedProcedure, router } from "../index";
import { z } from "zod";
import { apiKeys } from "@hoarder/db/schema";
import { eq, and } from "drizzle-orm";
diff --git a/packages/web/server/api/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts
index 626a7250..724a9998 100644
--- a/packages/web/server/api/routers/bookmarks.test.ts
+++ b/packages/trpc/routers/bookmarks.test.ts
@@ -1,4 +1,4 @@
-import { CustomTestContext, defaultBeforeEach } from "@/lib/testUtils";
+import { CustomTestContext, defaultBeforeEach } from "../testUtils";
import { expect, describe, test, beforeEach, assert } from "vitest";
beforeEach<CustomTestContext>(defaultBeforeEach(true));
diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts
index 73818508..ea7ffef8 100644
--- a/packages/web/server/api/routers/bookmarks.ts
+++ b/packages/trpc/routers/bookmarks.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { Context, authedProcedure, router } from "../trpc";
+import { Context, authedProcedure, router } from "../index";
import { getSearchIdxClient } from "@hoarder/shared/search";
import {
ZBookmark,
@@ -10,7 +10,7 @@ import {
zGetBookmarksResponseSchema,
zNewBookmarkRequestSchema,
zUpdateBookmarksRequestSchema,
-} from "@/lib/types/api/bookmarks";
+} from "../types/bookmarks";
import {
bookmarkLinks,
bookmarkTags,
@@ -25,7 +25,7 @@ import {
} from "@hoarder/shared/queues";
import { TRPCError, experimental_trpcMiddleware } from "@trpc/server";
import { and, desc, eq, inArray } from "drizzle-orm";
-import { ZBookmarkTags } from "@/lib/types/api/tags";
+import { ZBookmarkTags } from "../types/tags";
import { db as DONT_USE_db } from "@hoarder/db";
diff --git a/packages/web/server/api/routers/lists.ts b/packages/trpc/routers/lists.ts
index 7bf5eed5..fa97929d 100644
--- a/packages/web/server/api/routers/lists.ts
+++ b/packages/trpc/routers/lists.ts
@@ -1,10 +1,10 @@
-import { Context, authedProcedure, router } from "../trpc";
+import { Context, authedProcedure, router } from "../index";
import { SqliteError } from "@hoarder/db";
import { z } from "zod";
import { TRPCError, experimental_trpcMiddleware } from "@trpc/server";
import { bookmarkLists, bookmarksInLists } from "@hoarder/db/schema";
import { and, eq } from "drizzle-orm";
-import { zBookmarkListSchema } from "@/lib/types/api/lists";
+import { zBookmarkListSchema } from "../types/lists";
const ensureListOwnership = experimental_trpcMiddleware<{
ctx: Context;
diff --git a/packages/web/server/api/routers/users.test.ts b/packages/trpc/routers/users.test.ts
index 1ee04f99..87814407 100644
--- a/packages/web/server/api/routers/users.test.ts
+++ b/packages/trpc/routers/users.test.ts
@@ -2,7 +2,7 @@ import {
CustomTestContext,
defaultBeforeEach,
getApiCaller,
-} from "@/lib/testUtils";
+} from "../testUtils";
import { expect, describe, test, beforeEach, assert } from "vitest";
beforeEach<CustomTestContext>(defaultBeforeEach(false));
diff --git a/packages/web/server/api/routers/users.ts b/packages/trpc/routers/users.ts
index 32d10860..b5334f99 100644
--- a/packages/web/server/api/routers/users.ts
+++ b/packages/trpc/routers/users.ts
@@ -1,8 +1,8 @@
-import { zSignUpSchema } from "@/lib/types/api/users";
-import { adminProcedure, publicProcedure, router } from "../trpc";
+import { zSignUpSchema } from "../types/users";
+import { adminProcedure, publicProcedure, router } from "../index";
import { SqliteError } from "@hoarder/db";
import { z } from "zod";
-import { hashPassword } from "@/server/auth";
+import { hashPassword } from "../auth";
import { TRPCError } from "@trpc/server";
import { users } from "@hoarder/db/schema";
import { count, eq } from "drizzle-orm";
diff --git a/packages/web/lib/testUtils.ts b/packages/trpc/testUtils.ts
index bad78463..d5f24def 100644
--- a/packages/web/lib/testUtils.ts
+++ b/packages/trpc/testUtils.ts
@@ -1,7 +1,7 @@
import { users } from "@hoarder/db/schema";
import { getInMemoryDB } from "@hoarder/db/drizzle";
-import { appRouter } from "@/server/api/routers/_app";
-import { createCallerFactory } from "@/server/api/trpc";
+import { appRouter } from "./routers/_app";
+import { createCallerFactory } from "./index";
export function getTestDB() {
return getInMemoryDB(true);
diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json
new file mode 100644
index 00000000..bf020b01
--- /dev/null
+++ b/packages/trpc/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@tsconfig/node21/tsconfig.json",
+ "include": ["**/*.ts"],
+ "exclude": ["node_modules"],
+ "compilerOptions": {
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "baseUrl": "./",
+ "esModuleInterop": true
+ }
+}
+
diff --git a/packages/web/lib/types/api/bookmarks.ts b/packages/trpc/types/bookmarks.ts
index 5fabc7ca..b61ab0e0 100644
--- a/packages/web/lib/types/api/bookmarks.ts
+++ b/packages/trpc/types/bookmarks.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { zBookmarkTagSchema } from "@/lib/types/api/tags";
+import { zBookmarkTagSchema } from "./tags";
export const zBookmarkedLinkSchema = z.object({
type: z.literal("link"),
diff --git a/packages/web/lib/types/api/lists.ts b/packages/trpc/types/lists.ts
index 4b0ccaca..4b0ccaca 100644
--- a/packages/web/lib/types/api/lists.ts
+++ b/packages/trpc/types/lists.ts
diff --git a/packages/web/lib/types/api/tags.ts b/packages/trpc/types/tags.ts
index 7a99dad4..7a99dad4 100644
--- a/packages/web/lib/types/api/tags.ts
+++ b/packages/trpc/types/tags.ts
diff --git a/packages/web/lib/types/api/users.ts b/packages/trpc/types/users.ts
index c2fe182a..c2fe182a 100644
--- a/packages/web/lib/types/api/users.ts
+++ b/packages/trpc/types/users.ts
diff --git a/packages/trpc/vitest.config.ts b/packages/trpc/vitest.config.ts
new file mode 100644
index 00000000..c3d02f71
--- /dev/null
+++ b/packages/trpc/vitest.config.ts
@@ -0,0 +1,14 @@
+/// <reference types="vitest" />
+
+import { defineConfig } from "vitest/config";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [tsconfigPaths()],
+ test: {
+ alias: {
+ "@/*": "./*",
+ },
+ },
+});