Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Adding Rate limiting to the OTP verify #210

Merged
merged 2 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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