diff --git a/package-lock.json b/package-lock.json index 78c6f9b..92df2ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1287,13 +1287,13 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1920,7 +1920,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", diff --git a/src/app/api/auth/resend-otp/route.ts b/src/app/api/auth/resend-otp/route.ts index 74fa16c..d6256bb 100644 --- a/src/app/api/auth/resend-otp/route.ts +++ b/src/app/api/auth/resend-otp/route.ts @@ -1,4 +1,3 @@ - import { db } from "@/lib/drizzle"; import { otps } from "@/lib/schema"; import { NextRequest, NextResponse } from "next/server"; @@ -8,16 +7,13 @@ import otpEmailTemplate from "@/lib/templates/otp-template"; import { auth } from "@/auth"; import mailer from "@/lib/mailer"; - - // `onboarding@uk-culture.org` email is for development only const senderEmail = process.env.SENDER_EMAIL || "onboarding@uk-culture.org"; - 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!), @@ -25,30 +21,34 @@ export async function POST(req: NextRequest) { 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, @@ -56,18 +56,12 @@ export async function POST(req: NextRequest) { 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.`, @@ -79,4 +73,4 @@ export async function POST(req: NextRequest) { console.log("[OTP_RESEND_ERROR]: ", error); return NextResponse.json({ error: error.message }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index 3566c3f..0d741fd 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -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), @@ -42,16 +92,18 @@ 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 }); @@ -59,4 +111,4 @@ export async function POST(req: NextRequest) { console.log("[OTP_ERROR]: ", error); return NextResponse.json({ error: error.message }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 27edf1f..5336d11 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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"); } diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 1124f79..217667c 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -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

Loading...

; - } + if (status === "loading") { + return

Loading...

; + } - 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 ( -
-

User Profile

-
- User Avatar -

- Name: {session?.user?.name || "N/A"} -

-

- Email: {session?.user?.email || "N/A"} -

-
- -
- ); + return ( +
+

User Profile

+
+ User Avatar +

+ Name: {session?.user?.name || "N/A"} +

+

+ Email: {session?.user?.email || "N/A"} +

+
+ +
+ ); } diff --git a/src/components/ui/Navbar.tsx b/src/components/ui/Navbar.tsx index 70a0161..2eda5d4 100644 --- a/src/components/ui/Navbar.tsx +++ b/src/components/ui/Navbar.tsx @@ -27,7 +27,8 @@ export default function Navbar() { > User Avatar + className={styles["avatar"]} + width={250} height={250}/> ) : (