diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-07-12 12:33:23 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-07-12 12:33:23 +0000 |
| commit | 8e3013ba96532cab61eb6e5fae2ce30be5e94a57 (patch) | |
| tree | b101a0ac7e1218ea2562178e055044f315b0c245 /packages/trpc | |
| parent | 140311d7419fa2192e5149df8f589c3c3733a399 (diff) | |
| download | karakeep-8e3013ba96532cab61eb6e5fae2ce30be5e94a57.tar.zst | |
refactor: Move db interactions into the trpc routes
Diffstat (limited to 'packages/trpc')
| -rw-r--r-- | packages/trpc/email.ts | 32 | ||||
| -rw-r--r-- | packages/trpc/routers/invites.test.ts | 26 | ||||
| -rw-r--r-- | packages/trpc/routers/users.test.ts | 6 | ||||
| -rw-r--r-- | packages/trpc/routers/users.ts | 35 |
4 files changed, 66 insertions, 33 deletions
diff --git a/packages/trpc/email.ts b/packages/trpc/email.ts index 1c0b8800..e69c4507 100644 --- a/packages/trpc/email.ts +++ b/packages/trpc/email.ts @@ -1,25 +1,16 @@ -import { randomBytes } from "crypto"; import { createTransport } from "nodemailer"; -import { db } from "@karakeep/db"; -import { passwordResetTokens, verificationTokens } from "@karakeep/db/schema"; import serverConfig from "@karakeep/shared/config"; -export async function sendVerificationEmail(email: string, name: string) { +export async function sendVerificationEmail( + email: string, + name: string, + token: string, +) { if (!serverConfig.email.smtp) { throw new Error("SMTP is not configured"); } - 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, - }); - const transporter = createTransport({ host: serverConfig.email.smtp.host, port: serverConfig.email.smtp.port, @@ -133,22 +124,12 @@ If you weren't expecting this invitation, you can safely ignore this email. export async function sendPasswordResetEmail( email: string, name: string, - userId: string, + token: string, ) { if (!serverConfig.email.smtp) { throw new Error("SMTP is not configured"); } - const token = randomBytes(32).toString("hex"); - const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour - - // Store password reset token - await db.insert(passwordResetTokens).values({ - userId, - token, - expires, - }); - const transporter = createTransport({ host: serverConfig.email.smtp.host, port: serverConfig.email.smtp.port, @@ -197,5 +178,4 @@ If you didn't request a password reset, please ignore this email. Your password }; await transporter.sendMail(mailOptions); - return token; } 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<CustomTestContext>(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); |
