Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | 7x 7x 7x 25x 25x 25x 1x 24x 24x 24x 24x 24x 21x 21x 1x 20x 20x 20x 1x 19x 2x 1x | import crypto from "node:crypto";
import { eq, and, gt } from "drizzle-orm";
import { db } from "../db/index.js";
import { otpCodes } from "../db/schema.js";
const OTP_MAX_ATTEMPTS = 5;
const OTP_TTL_MINUTES = 10;
const OTP_RATE_LIMIT_PER_HOUR = 3;
/** Rate-limit, generate, store, and send an OTP for the given email. */
export async function requestOtp(email: string): Promise<void> {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const recentCodes = await db
.select()
.from(otpCodes)
.where(and(eq(otpCodes.email, email), gt(otpCodes.createdAt, oneHourAgo)));
if (recentCodes.length >= OTP_RATE_LIMIT_PER_HOUR) {
throw new OtpRateLimitError("Too many OTP requests. Try again later.");
}
const code = crypto.randomInt(100_000, 999_999).toString();
const expiresAt = new Date(Date.now() + OTP_TTL_MINUTES * 60 * 1000);
await db.insert(otpCodes).values({ email, code, expiresAt });
sendOtp(email, code);
}
function sendOtp(email: string, code: string): void {
console.log(`[OTP] Code for ${email}: ${code}`);
}
/** Validate an OTP code for the given email. Returns the email on success, throws on failure. */
export async function verifyOtp(email: string, code: string): Promise<void> {
const [otp] = await db
.select()
.from(otpCodes)
.where(
and(
eq(otpCodes.email, email),
eq(otpCodes.used, false),
gt(otpCodes.expiresAt, new Date()),
),
)
.orderBy(otpCodes.createdAt)
.limit(1);
if (!otp) {
throw new OtpError("Invalid or expired code");
}
Iif (otp.attempts >= OTP_MAX_ATTEMPTS) {
await db.update(otpCodes).set({ used: true }).where(eq(otpCodes.id, otp.id));
throw new OtpError("Too many attempts. Request a new code.");
}
await db
.update(otpCodes)
.set({ attempts: otp.attempts + 1 })
.where(eq(otpCodes.id, otp.id));
if (otp.code !== code) {
throw new OtpError("Invalid or expired code");
}
await db.update(otpCodes).set({ used: true }).where(eq(otpCodes.id, otp.id));
}
export class OtpError extends Error {
constructor(message: string) {
super(message);
}
}
export class OtpRateLimitError extends Error {
constructor(message: string) {
super(message);
}
}
|