From 93049e864ae6d281b60c23dee868bca3f585dd4a Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Thu, 10 Jul 2025 08:35:32 +0000 Subject: feat: Add support for email verification --- packages/trpc/routers/users.ts | 90 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) (limited to 'packages/trpc/routers/users.ts') diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index 17c9fa3a..58093b42 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -26,6 +26,7 @@ import { } from "@karakeep/shared/types/users"; import { generatePasswordSalt, hashPassword, validatePassword } from "../auth"; +import { sendVerificationEmail, verifyEmailToken } from "../email"; import { adminProcedure, authedProcedure, @@ -102,13 +103,23 @@ export async function createUser( role?: "user" | "admin", ) { const salt = generatePasswordSalt(); - return await createUserRaw(ctx.db, { + let user = await createUserRaw(ctx.db, { name: input.name, email: input.email, password: await hashPassword(input.password, salt), salt, role, }); + // Send verification email if required + if (serverConfig.auth.emailVerificationRequired) { + try { + await sendVerificationEmail(input.email, input.name); + } catch (error) { + console.error("Failed to send verification email:", error); + // Don't fail user creation if email sending fails + } + } + return user; } export const usersAppRouter = router({ @@ -529,4 +540,81 @@ export const usersAppRouter = router({ }) .where(eq(userSettings.userId, ctx.user.id)); }), + verifyEmail: publicProcedure + .input( + z.object({ + email: z.string().email(), + token: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const isValid = await verifyEmailToken(input.email, input.token); + if (!isValid) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid or expired verification token", + }); + } + + // Update user's emailVerified status + const result = await ctx.db + .update(users) + .set({ emailVerified: new Date() }) + .where(eq(users.email, input.email)); + + if (result.changes === 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + + return { success: true }; + }), + resendVerificationEmail: publicProcedure + .input( + z.object({ + email: z.string().email(), + }), + ) + .mutation(async ({ input, ctx }) => { + if ( + !serverConfig.auth.emailVerificationRequired || + !serverConfig.email.smtp + ) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Email verification is not enabled", + }); + } + + const user = await ctx.db.query.users.findFirst({ + where: eq(users.email, input.email), + }); + + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + + if (user.emailVerified) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Email is already verified", + }); + } + + try { + await sendVerificationEmail(input.email, user.name); + return { success: true }; + } catch (error) { + console.error("Failed to send verification email:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to send verification email", + }); + } + }), }); -- cgit v1.2.3-70-g09d2