Download packages/api/src/routers/verification.ts from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 6.51 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/routers/verification.ts
- Command line
-
hf download hf://spaces/Atmint/cpns/packages/api/src/routers/verification.ts
-
curl -L -o verification.ts https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/routers/verification.ts
6.51 kB
| import { db } from "@pram/db"; | |
| import { verification, user, account } from "@pram/db/schema/auth"; | |
| import { TRPCError } from "@trpc/server"; | |
| import bcrypt from "bcryptjs"; | |
| import crypto from "node:crypto"; | |
| import { eq, and, gt, sql } from "drizzle-orm"; | |
| import { z } from "zod"; | |
| import { sendOtpEmail } from "../lib/email"; | |
| import { autoRefillIfEligible } from "../lib/credit"; | |
| import { publicProcedure, router } from "../index"; | |
| import { checkRateLimit } from "../lib/rate-limit"; | |
| import { validateTurnstileToken } from "../lib/turnstile"; | |
| const OTP_LENGTH = 6; | |
| const OTP_EXPIRY_MS = 5 * 60 * 1000; | |
| function generateOtp(): string { | |
| return crypto.randomInt(10 ** (OTP_LENGTH - 1), 10 ** OTP_LENGTH - 1).toString(); | |
| } | |
| function genericOtpResponse() { | |
| return { success: true, message: "If the email is registered, an OTP has been sent." }; | |
| } | |
| export const verificationRouter = router({ | |
| sendVerificationOtp: publicProcedure | |
| .input(z.object({ email: z.string().email(), turnstileToken: z.string().optional() })) | |
| .mutation(async ({ input, ctx }) => { | |
| const ip = ctx.ip ?? "unknown"; | |
| await checkRateLimit({ key: `otp-send:email:${input.email}`, limit: 3, windowMs: 300_000, strict: true }); | |
| await checkRateLimit({ key: `otp-send:ip:${ip}:email`, limit: 20, windowMs: 900_000, strict: true }); | |
| await validateTurnstileToken(input.turnstileToken); | |
| const [existingUser] = await db | |
| .select({ id: user.id, emailVerified: user.emailVerified }) | |
| .from(user) | |
| .where(eq(user.email, input.email)) | |
| .limit(1); | |
| if (!existingUser || existingUser.emailVerified) { | |
| return genericOtpResponse(); | |
| } | |
| const otp = generateOtp(); | |
| const identifier = `email-verification:${input.email}`; | |
| await db.delete(verification).where(eq(verification.identifier, identifier)); | |
| await db.insert(verification).values({ | |
| id: crypto.randomUUID(), | |
| identifier, | |
| value: otp, | |
| expiresAt: new Date(Date.now() + OTP_EXPIRY_MS), | |
| }); | |
| await sendOtpEmail({ to: input.email, otp, type: "email-verification" }); | |
| return genericOtpResponse(); | |
| }), | |
| verifyEmailOtp: publicProcedure | |
| .input(z.object({ email: z.string().email(), otp: z.string().length(6) })) | |
| .mutation(async ({ input, ctx }) => { | |
| const ip = ctx.ip ?? "unknown"; | |
| await checkRateLimit({ key: `otp-verify:email:${input.email}`, limit: 10, windowMs: 300_000, strict: true }); | |
| await checkRateLimit({ key: `otp-verify:ip:${ip}`, limit: 30, windowMs: 300_000, strict: true }); | |
| const identifier = `email-verification:${input.email}`; | |
| const [record] = await db | |
| .select() | |
| .from(verification) | |
| .where( | |
| and( | |
| eq(verification.identifier, identifier), | |
| eq(verification.value, input.otp), | |
| gt(verification.expiresAt, sql`now()`), | |
| ), | |
| ) | |
| .limit(1); | |
| if (!record) { | |
| throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid or expired OTP" }); | |
| } | |
| await db | |
| .update(user) | |
| .set({ emailVerified: true }) | |
| .where(eq(user.email, input.email)); | |
| await db.delete(verification).where(eq(verification.id, record.id)); | |
| const [verifiedUser] = await db | |
| .select({ id: user.id }) | |
| .from(user) | |
| .where(eq(user.email, input.email)) | |
| .limit(1); | |
| if (verifiedUser) { | |
| await autoRefillIfEligible(verifiedUser.id); | |
| } | |
| return { success: true }; | |
| }), | |
| sendPasswordResetOtp: publicProcedure | |
| .input(z.object({ email: z.string().email(), turnstileToken: z.string().optional() })) | |
| .mutation(async ({ input, ctx }) => { | |
| const ip = ctx.ip ?? "unknown"; | |
| await checkRateLimit({ key: `otp-send:email:${input.email}`, limit: 3, windowMs: 300_000, strict: true }); | |
| await checkRateLimit({ key: `otp-send:ip:${ip}:reset`, limit: 20, windowMs: 900_000, strict: true }); | |
| await validateTurnstileToken(input.turnstileToken); | |
| const [existingUser] = await db | |
| .select({ id: user.id }) | |
| .from(user) | |
| .where(eq(user.email, input.email)) | |
| .limit(1); | |
| if (!existingUser) { | |
| return genericOtpResponse(); | |
| } | |
| const otp = generateOtp(); | |
| const identifier = `forget-password:${input.email}`; | |
| await db.delete(verification).where(eq(verification.identifier, identifier)); | |
| await db.insert(verification).values({ | |
| id: crypto.randomUUID(), | |
| identifier, | |
| value: otp, | |
| expiresAt: new Date(Date.now() + OTP_EXPIRY_MS), | |
| }); | |
| await sendOtpEmail({ to: input.email, otp, type: "forget-password" }); | |
| return genericOtpResponse(); | |
| }), | |
| resetPassword: publicProcedure | |
| .input( | |
| z.object({ | |
| email: z.string().email(), | |
| otp: z.string().length(6), | |
| newPassword: z.string().regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/, "Password must be at least 8 characters with uppercase, lowercase, and number"), | |
| }), | |
| ) | |
| .mutation(async ({ input, ctx }) => { | |
| const ip = ctx.ip ?? "unknown"; | |
| await checkRateLimit({ key: `otp-reset:email:${input.email}`, limit: 5, windowMs: 300_000, strict: true }); | |
| await checkRateLimit({ key: `otp-reset:ip:${ip}`, limit: 15, windowMs: 300_000, strict: true }); | |
| const identifier = `forget-password:${input.email}`; | |
| const [record] = await db | |
| .select() | |
| .from(verification) | |
| .where( | |
| and( | |
| eq(verification.identifier, identifier), | |
| eq(verification.value, input.otp), | |
| gt(verification.expiresAt, sql`now()`), | |
| ), | |
| ) | |
| .limit(1); | |
| if (!record) { | |
| throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid or expired OTP" }); | |
| } | |
| const passwordHash = await bcrypt.hash(input.newPassword, 10); | |
| const [userRecord] = await db | |
| .select({ id: user.id, name: user.name }) | |
| .from(user) | |
| .where(eq(user.email, input.email)) | |
| .limit(1); | |
| if (!userRecord) { | |
| throw new TRPCError({ code: "NOT_FOUND", message: "User not found" }); | |
| } | |
| await db | |
| .update(account) | |
| .set({ password: passwordHash }) | |
| .where( | |
| and(eq(account.providerId, "credential"), eq(account.userId, userRecord.id)), | |
| ); | |
| await db.delete(verification).where(eq(verification.id, record.id)); | |
| return { success: true }; | |
| }), | |
| }); | |