Skip to content

Commit

Permalink
Merge pull request #210 from RishabhRawatt/main
Browse files Browse the repository at this point in the history
[FEATURE] Adding Rate limiting to the OTP verify
  • Loading branch information
ajaynegi45 authored Oct 26, 2024
2 parents 891fcc3 + 9d3a286 commit 54bfb0d
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 72 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 15 additions & 21 deletions src/app/api/auth/resend-otp/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { db } from "@/lib/drizzle";
import { otps } from "@/lib/schema";
import { NextRequest, NextResponse } from "next/server";
Expand All @@ -8,66 +7,61 @@ import otpEmailTemplate from "@/lib/templates/otp-template";
import { auth } from "@/auth";
import mailer from "@/lib/mailer";



// `[email protected]` email is for development only
const senderEmail = process.env.SENDER_EMAIL || "[email protected]";


export async function POST(req: NextRequest) {
try {
const session = await auth();
if (!session) throw new Error("Login first to verify email")
if (!session) throw new Error("Login first to verify email");

const userWithEmail = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.email, session.user.email!),
});

if (!userWithEmail) throw new Error("User with email not exists.");


const prevOtps = await db.query.otps.findMany({
where: (otps, { eq }) => eq(otps.userId, userWithEmail.id),
orderBy: (otps, { desc }) => desc(otps.expiresAt),
})
});

// Check if there is a valid OTP
const now = new Date();
const validOtp = prevOtps.find(otp => otp.expiresAt > now);
const validOtp = prevOtps.find((otp) => otp.expiresAt > now);

if (validOtp) {
const remainingTimeInSeconds = Math.floor((validOtp.expiresAt.getTime() - now.getTime()) / 1000); // Time in seconds
const remainingTimeInSeconds = Math.floor(
(validOtp.expiresAt.getTime() - now.getTime()) / 1000
); // Time in seconds
const minutes = Math.floor(remainingTimeInSeconds / 60); // Full minutes
const seconds = remainingTimeInSeconds % 60; // Remaining seconds

return NextResponse.json({ message: `Please wait ${minutes} minutes and ${seconds} seconds before resending the OTP.` }, { status: 429 });
return NextResponse.json(
{
message: `Please wait ${minutes} minutes and ${seconds} seconds before resending the OTP.`,
},
{ status: 429 }
);
}

const otp = Math.floor(100000 + Math.random() * 900000).toString(); // 6-digit OTP
// Set expiration time (e.g., 10 minutes from now)
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes in the future



await db.insert(otps).values({
expiresAt,
otp,
userId: userWithEmail.id,
createdAt: new Date(),
});




await mailer.sendMail({
from: `Uttarakhand Culture <${senderEmail}>`,
to: [userWithEmail.email!],
subject: 'Verify you email with OTP',
subject: "Verify you email with OTP",
html: otpEmailTemplate(userWithEmail.name!, otp),
})



});

return NextResponse.json({
message: `OTP has been resent to your email.`,
Expand All @@ -79,4 +73,4 @@ export async function POST(req: NextRequest) {
console.log("[OTP_RESEND_ERROR]: ", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
}
80 changes: 66 additions & 14 deletions src/app/api/auth/verify-otp/route.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,78 @@

import { db } from "@/lib/drizzle";
import { otps, users } from "@/lib/schema";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

import { auth } from "@/auth";
import { eq } from "drizzle-orm";

// In-memory store for rate limiting
const rateLimit = new Map<
string,
{ requests: number; lastRequestTime: number }
>();

// Rate limiting configuration
const MAX_REQUESTS = 3; // Max requests per window
const WINDOW_MS = 5 * 60 * 1000; // 5 minutes in milliseconds

function checkRateLimit(userId: string): boolean {
const currentTime = Date.now();
const userData = rateLimit.get(userId);

if (userData) {
// Reset if the window has passed
if (currentTime - userData.lastRequestTime > WINDOW_MS) {
rateLimit.set(userId, { requests: 1, lastRequestTime: currentTime });
return false;
}
// Increment request count if within window
if (userData.requests < MAX_REQUESTS) {
rateLimit.set(userId, {
requests: userData.requests + 1,
lastRequestTime: currentTime,
});
return false;
}
// Rate limit exceeded
return true;
} else {
// Initialize user rate limit
rateLimit.set(userId, { requests: 1, lastRequestTime: currentTime });
return false;
}
}

export async function POST(req: NextRequest) {
try {
const session = await auth();
if (!session) throw new Error("Login first to verify email")
if (!session) throw new Error("Login first to verify email");

const userId = session.user.id;

// Check rate limit
if (checkRateLimit(userId)) {
return NextResponse.json(
{ error: "limit exceeded. Please try again after 5 minutes." },
{ status: 429 }
);
}

if (session.user.emailVerified) {
return NextResponse.json({ message: "Email is already verified!" }, { status: 200 });
return NextResponse.json(
{ message: "Email is already verified!" },
{ status: 200 }
);
}

const { otp } = await req.json()
if (!otp) throw new Error("OTP is required")
const { otp } = await req.json();
if (!otp) throw new Error("OTP is required");

const userWithEmail = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.email, session.user.email!),
});

if (!userWithEmail) throw new Error("User with email not exists.");

if (!userWithEmail) throw new Error("User with email does not exist.");

const latestOtp = await db.query.otps.findFirst({
where: (otps, { eq }) => eq(otps.userId, userWithEmail.id),
Expand All @@ -42,21 +92,23 @@ export async function POST(req: NextRequest) {
throw new Error("Invalid OTP.");
}

// If OTP is valid, mark email as verified (you can implement this as per your requirement)
// await db.update(users).set({ emailVerified: new Date() }).where((users, { eq }) => eq(users.id, userWithEmail.id));
await db.update(users).set({ emailVerified: new Date() })
// Mark email as verified
await db
.update(users)
.set({ emailVerified: new Date() })
.where(eq(users.id, userWithEmail.id));


await db.delete(otps).where(eq(otps.userId, userWithEmail.id));

return NextResponse.json({ message: "Email verified successfully!" }, { status: 200 });

return NextResponse.json(
{ message: "Email verified successfully!" },
{ status: 200 }
);
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors }, { status: 500 });
}
console.log("[OTP_ERROR]: ", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
}
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default async function RootLayout({
const pathname = h.get("x-current-path");

if (session) {
if (!session.user.emailVerified) {
if (!session.user?.emailVerified) {
if (!pathname!.includes("/auth")) {
redirect("/auth/verify");
}
Expand Down
69 changes: 37 additions & 32 deletions src/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
"use client";
import {useSession, signOut} from "next-auth/react";
import {useRouter} from "next/navigation";
import { useSession, signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import styles from "./profile.module.css";
import Image from "next/image";

export default function Profile() {
const {data: session, status} = useSession();
const router = useRouter();
const { data: session, status } = useSession();
const router = useRouter();

if (status === "loading") {
return <p>Loading...</p>;
}
if (status === "loading") {
return <p>Loading...</p>;
}

if (status === "unauthenticated") {
router.push("/"); // Redirect to login if unauthenticated
return null;
}
if (status === "unauthenticated") {
router.push("/"); // Redirect to login if unauthenticated
return null;
}

return (
<div className={styles["profile-container"]}>
<h1 className={styles["profile-heading"]}>User Profile</h1>
<div className={styles["profile-details"]}>
<Image src={session?.user?.image || "/default-avatar.png"} alt="User Avatar"
className={styles["profile-avatar"]}/>
<p>
<strong>Name:</strong> {session?.user?.name || "N/A"}
</p>
<p>
<strong>Email:</strong> {session?.user?.email || "N/A"}
</p>
</div>
<button
onClick={() => signOut({redirect: false})}
className={styles["logout-button"]}
>
Logout
</button>
</div>
);
return (
<div className={styles["profile-container"]}>
<h1 className={styles["profile-heading"]}>User Profile</h1>
<div className={styles["profile-details"]}>
<Image
src={session?.user?.image || "/default-avatar.png"}
alt="User Avatar"
className={styles["profile-avatar"]}
width={250}
height={250}
/>
<p>
<strong>Name:</strong> {session?.user?.name || "N/A"}
</p>
<p>
<strong>Email:</strong> {session?.user?.email || "N/A"}
</p>
</div>
<button
onClick={() => signOut({ redirect: false })}
className={styles["logout-button"]}
>
Logout
</button>
</div>
);
}
3 changes: 2 additions & 1 deletion src/components/ui/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export default function Navbar() {
>
<Image src={session.data?.user?.image || "/default-avatar.png"}
alt="User Avatar"
className={styles["avatar"]}/>
className={styles["avatar"]}
width={250} height={250}/>

</div>
) : (
Expand Down

0 comments on commit 54bfb0d

Please sign in to comment.