From 8e3013ba96532cab61eb6e5fae2ce30be5e94a57 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 12 Jul 2025 12:33:23 +0000 Subject: refactor: Move db interactions into the trpc routes --- packages/trpc/routers/invites.test.ts | 26 ++++++++++++++++++++++++++ packages/trpc/routers/users.test.ts | 6 +++--- packages/trpc/routers/users.ts | 35 +++++++++++++++++++++++++++++++---- 3 files changed, 60 insertions(+), 7 deletions(-) (limited to 'packages/trpc/routers') diff --git a/packages/trpc/routers/invites.test.ts b/packages/trpc/routers/invites.test.ts index bb1209c4..f4f048df 100644 --- a/packages/trpc/routers/invites.test.ts +++ b/packages/trpc/routers/invites.test.ts @@ -6,6 +6,32 @@ import { invites, users } from "@karakeep/db/schema"; import type { CustomTestContext } from "../testUtils"; import { defaultBeforeEach, getApiCaller } from "../testUtils"; +// Mock server config with email settings +vi.mock("@karakeep/shared/config", async (original) => { + const mod = (await original()) as typeof import("@karakeep/shared/config"); + return { + ...mod, + default: { + ...mod.default, + email: { + smtp: { + host: "test-smtp.example.com", + port: 587, + secure: false, + user: "test@example.com", + password: "test-password", + from: "test@example.com", + }, + }, + }, + }; +}); + +// Mock email functions +vi.mock("../email", () => ({ + sendInviteEmail: vi.fn().mockResolvedValue(undefined), +})); + beforeEach(defaultBeforeEach(false)); describe("Invites Router", () => { diff --git a/packages/trpc/routers/users.test.ts b/packages/trpc/routers/users.test.ts index 03e5d590..1c03f47a 100644 --- a/packages/trpc/routers/users.test.ts +++ b/packages/trpc/routers/users.test.ts @@ -14,7 +14,7 @@ import type { CustomTestContext } from "../testUtils"; import * as emailModule from "../email"; import { defaultBeforeEach, getApiCaller } from "../testUtils"; -// Mock server config with all required properties - MUST be before any imports that use config +// Mock server config with email settings vi.mock("@karakeep/shared/config", async (original) => { const mod = (await original()) as typeof import("@karakeep/shared/config"); return { @@ -516,7 +516,7 @@ describe("User Routes", () => { unauthedAPICaller, }) => { // Create a user first - const user = await unauthedAPICaller.users.create({ + await unauthedAPICaller.users.create({ name: "Test User", email: "reset@test.com", password: "pass1234", @@ -534,7 +534,7 @@ describe("User Routes", () => { expect(emailModule.sendPasswordResetEmail).toHaveBeenCalledWith( "reset@test.com", "Test User", - user.id, + expect.any(String), // token ); }); diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index 79f06057..8d6db6c7 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -1,3 +1,4 @@ +import { randomBytes } from "crypto"; import { TRPCError } from "@trpc/server"; import { and, count, desc, eq, gte, sql } from "drizzle-orm"; import invariant from "tiny-invariant"; @@ -39,7 +40,21 @@ import { router, } from "../index"; -export async function verifyEmailToken( +async function genEmailVerificationToken(db: Context["db"], email: string) { + const token = randomBytes(10).toString("hex"); + const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours + + // Store verification token + await db.insert(verificationTokens).values({ + identifier: email, + token, + expires, + }); + + return token; +} + +async function verifyEmailToken( db: Context["db"], email: string, token: string, @@ -156,8 +171,9 @@ export async function createUser( }); // Send verification email if required if (serverConfig.auth.emailVerificationRequired) { + const token = await genEmailVerificationToken(ctx.db, input.email); try { - await sendVerificationEmail(input.email, input.name); + await sendVerificationEmail(input.email, input.name, token); } catch (error) { console.error("Failed to send verification email:", error); // Don't fail user creation if email sending fails @@ -671,8 +687,9 @@ export const usersAppRouter = router({ }); } + const token = await genEmailVerificationToken(ctx.db, input.email); try { - await sendVerificationEmail(input.email, user.name); + await sendVerificationEmail(input.email, user.name, token); return { success: true }; } catch (error) { console.error("Failed to send verification email:", error); @@ -718,7 +735,17 @@ export const usersAppRouter = router({ } try { - await sendPasswordResetEmail(input.email, user.name, user.id); + const token = randomBytes(32).toString("hex"); + const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour + + // Store password reset token + await ctx.db.insert(passwordResetTokens).values({ + userId: user.id, + token, + expires, + }); + + await sendPasswordResetEmail(input.email, user.name, token); return { success: true }; } catch (error) { console.error("Failed to send password reset email:", error); -- cgit v1.2.3-70-g09d2