diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-07-10 08:35:32 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-07-10 08:37:44 +0000 |
| commit | 93049e864ae6d281b60c23dee868bca3f585dd4a (patch) | |
| tree | d39c0b4221486dbc82461a505f205d162a9e4def /packages/trpc/routers/users.ts | |
| parent | aae3ef17eccf0752edb5ce5638a58444ccb6ce3a (diff) | |
| download | karakeep-93049e864ae6d281b60c23dee868bca3f585dd4a.tar.zst | |
feat: Add support for email verification
Diffstat (limited to 'packages/trpc/routers/users.ts')
| -rw-r--r-- | packages/trpc/routers/users.ts | 90 |
1 files changed, 89 insertions, 1 deletions
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", + }); + } + }), }); |
