Skip to content
Open
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ MAILGUN_API_KEY=""
MAILGUN_DOMAIN=""
# MAILGUN_URL="https://api.eu.mailgun.net" # Uncomment if using an EU region domain
EMAIL_FROM="no-reply@yourdomain.com"

# Auth
AUTH_SECRET=""
AUTH_URL=""

# Google
GOOGLE_CLIENT_ID="your-client-id-here.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-client-secret-here"
3 changes: 3 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { handlers } from "@/auth"

export const { GET, POST } = handlers
41 changes: 41 additions & 0 deletions app/api/verify-email/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/mongodb";
import { redisClient } from "@/lib/ratelimit";

export async function GET(req: NextRequest) {
const token = req.nextUrl.searchParams.get("token");

if (!token || !redisClient) {
return NextResponse.redirect(new URL("/login?error=InvalidToken", req.url));
}

// 1. Retrieve pending data from Redis
const rawData = await redisClient.get(`verify_${token}`);
if (!rawData) {
return NextResponse.redirect(new URL("/login?error=TokenExpired", req.url));
}

const pendingUser = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
Comment on lines +8 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle Redis/JSON failures explicitly.

redisClient.get() and JSON.parse() can both throw here. A Redis outage or malformed verify_${token} payload will currently bubble up as a 500 and break the sign-up flow. Also validate the parsed payload before using it.

Suggested fix
 export async function GET(req: NextRequest) {
   const token = req.nextUrl.searchParams.get("token");
 
-  if (!token || !redisClient) {
+  if (!token) {
     return NextResponse.redirect(new URL("/login?error=InvalidToken", req.url));
   }
+  if (!redisClient) {
+    return NextResponse.redirect(new URL("/login?error=AuthUnavailable", req.url));
+  }
 
-  // 1. Retrieve pending data from Redis
-  const rawData = await redisClient.get(`verify_${token}`);
-  if (!rawData) {
-    return NextResponse.redirect(new URL("/login?error=TokenExpired", req.url));
-  }
-
-  const pendingUser = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
+  let pendingUser;
+  try {
+    const rawData = await redisClient.get(`verify_${token}`);
+    if (!rawData) {
+      return NextResponse.redirect(new URL("/login?error=TokenExpired", req.url));
+    }
+
+    pendingUser = typeof rawData === "string" ? JSON.parse(rawData) : rawData;
+    if (!pendingUser?.email || !pendingUser?.hashedPassword) {
+      return NextResponse.redirect(new URL("/login?error=InvalidToken", req.url));
+    }
+  } catch (error) {
+    console.error("Failed to read or parse verification payload", error);
+    return NextResponse.redirect(new URL("/login?error=AuthUnavailable", req.url));
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!token || !redisClient) {
return NextResponse.redirect(new URL("/login?error=InvalidToken", req.url));
}
// 1. Retrieve pending data from Redis
const rawData = await redisClient.get(`verify_${token}`);
if (!rawData) {
return NextResponse.redirect(new URL("/login?error=TokenExpired", req.url));
}
const pendingUser = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
if (!token) {
return NextResponse.redirect(new URL("/login?error=InvalidToken", req.url));
}
if (!redisClient) {
return NextResponse.redirect(new URL("/login?error=AuthUnavailable", req.url));
}
let pendingUser;
try {
const rawData = await redisClient.get(`verify_${token}`);
if (!rawData) {
return NextResponse.redirect(new URL("/login?error=TokenExpired", req.url));
}
pendingUser = typeof rawData === "string" ? JSON.parse(rawData) : rawData;
if (!pendingUser?.email || !pendingUser?.hashedPassword) {
return NextResponse.redirect(new URL("/login?error=InvalidToken", req.url));
}
} catch (error) {
console.error("Failed to read or parse verification payload", error);
return NextResponse.redirect(new URL("/login?error=AuthUnavailable", req.url));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/verify-email/route.ts` around lines 8 - 18, Wrap the call to
redisClient.get(`verify_${token}`) and the JSON.parse(rawData) in a try/catch so
Redis errors or malformed JSON don't bubble up; on error or parse failure log
the error and return a safe redirect (e.g. "/login?error=TokenExpired" or
"/login?error=InvalidToken") instead of throwing. After parsing, validate the
resulting pendingUser object fields you plan to use (e.g. ensure
pendingUser.email and pendingUser.userId exist and are the right type) and treat
invalid/missing payloads the same way (log + redirect). Ensure you reference
redisClient.get, JSON.parse, and pendingUser in these checks so the flow fails
gracefully.


// 2. Atomic Save to MongoDB
const db = (await clientPromise).db();

// Atomic upsert — safe under concurrent clicks / retries.
await db.collection("users").updateOne(
{ email: pendingUser.email },
{
$setOnInsert: {
...pendingUser,
emailVerified: new Date(),
createdAt: new Date(),
},
},
{ upsert: true }
);

// 3. Delete the token from Redis
await redisClient.del(`verify_${token}`);
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make Redis token deletion best-effort.

If del() fails after MongoDB has already accepted the account, the request still ends as a 500 even though verification succeeded. Treat cleanup as non-blocking and continue to the success redirect.

Suggested fix
-  // 3. Delete the token from Redis
-  await redisClient.del(`verify_${token}`);
+  // 3. Delete the token from Redis (best-effort cleanup)
+  try {
+    await redisClient.del(`verify_${token}`);
+  } catch (error) {
+    console.warn("Failed to delete verification token", error);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 3. Delete the token from Redis
await redisClient.del(`verify_${token}`);
// 3. Delete the token from Redis (best-effort cleanup)
try {
await redisClient.del(`verify_${token}`);
} catch (error) {
console.warn("Failed to delete verification token", error);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/verify-email/route.ts` around lines 36 - 37, Wrap the Redis deletion
call so it’s best-effort: catch errors from await
redisClient.del(`verify_${token}`) (or avoid awaiting) and log the failure
instead of letting it propagate; ensure the rest of the route (the success
redirect after account verification) still runs even if Redis del fails. Use the
existing redisClient.del call and token variable, and log the error with the
project logger or console.error inside the catch block so cleanup never turns
into a 500 response.


// 4. Redirect them to login
return NextResponse.redirect(new URL("/login?success=EmailVerified", req.url));
}
2 changes: 2 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

@theme inline {
--color-background: var(--background);
--color-background-700: var(--background-700);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-surface: var(--surface);
Expand All @@ -32,6 +33,7 @@
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0f1c;
--background-700: #1d2846;
--foreground: #e8ecf5;
--muted: #97a1b5;
--surface: #111729;
Expand Down
64 changes: 64 additions & 0 deletions app/login/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use server"

import { signIn } from "@/auth"
import { AuthError } from "next-auth"
import { redirect } from "next/navigation"
import { limit } from "@/lib/ratelimit"

export async function signInWithMagicLink(formData: FormData) {
const email = formData.get("email") as string;

if (!email || !/@iiitl\.ac\.in$/i.test(email)) {
redirect("/login?error=AccessDenied")
}

// Pre-check rate limits before calling Auth.js
const { success } = await limit(`magic_link_${email}`, {
maxRequests: 5,
window: '1h'
});

if (!success) {
redirect("/login?error=AccessDenied")
}

try {
await signIn("email", { email, redirectTo: "/" })
} catch (error) {
if (error instanceof AuthError) {
redirect("/login?error=AccessDenied")
}
throw error
}
Comment on lines +25 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

NextAuth.js v5 how to throw AccessDenied or custom AuthError from provider sendVerificationRequest

💡 Result:

In NextAuth.js (Auth.js) v5, throwing a custom AccessDenied or AuthError from the sendVerificationRequest function in an email provider is not directly supported for client-side propagation like in the Credentials provider's authorize callback. Throwing any Error from sendVerificationRequest will log a server-side error (typically [auth][error][SIGNIN_EMAIL_ERROR] or CallbackRouteError) but results in a client response of {error: "EmailSignin", status: 200, ok: true} instead of propagating the custom error to the client with ok: false. This is a known issue (open as of 2025). To throw custom errors: 1. Import { AuthError, AccessDenied } from "next-auth". 2. In your custom sendVerificationRequest: async sendVerificationRequest(params) { // Your validation logic if (someCondition) { throw new AccessDenied("Access denied: reason"); } // Send email... } However, due to the bug, the client receives generic "EmailSignin" instead of your custom message. Errors are caught server-side but not mapped to custom client responses for email providers. Workarounds: - Use server-side validation before calling signIn('email', { email }) and throw AuthError there (catchable client-side). - Custom error pages via pages: { error: '/custom-error' } handle generic errors. - For Credentials providers, extend specific errors like CredentialsSignin works better. Official docs confirm sendVerificationRequest expects Promise and throwing Error is standard for send failures, but client handling differs from authorize. Example config: providers: [{ id: "email", type: "email", async sendVerificationRequest({ identifier }) { if (!allowEmail(identifier)) { throw new AccessDenied("Email not allowed"); } // send email } }]

Citations:


🏁 Script executed:

find . -name "auth.ts" -o -name "auth.config.ts" | head -20

Repository: iiitl/alumni

Length of output: 65


🏁 Script executed:

find . -name "actions.ts" | grep -i login

Repository: iiitl/alumni

Length of output: 78


🏁 Script executed:

cat -n ./auth.ts

Repository: iiitl/alumni

Length of output: 5392


🏁 Script executed:

cat -n ./app/login/actions.ts

Repository: iiitl/alumni

Length of output: 1847


Magic-link errors are silently discarded by NextAuth.js v5 internally.

The sendVerificationRequest hook in auth.ts throws plain Error("Rate limit exceeded") and Error("Failed to send verification email.") when rate limits or email delivery fails. However, NextAuth.js v5 catches these errors server-side and responds with a generic {error: "EmailSignin", status: 200, ok: true} to the client. The catch block here never intercepts them because signIn("email", ...) appears to succeed before these errors are logged, resulting in a silent failure and redirect to "/" even when rate-limited or delivery fails.

Validate the email and rate limit before calling signIn(), and throw an AuthError subclass there (e.g., new AccessDenied("Rate limited")). This will propagate to the catch block and redirect to the error page as intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/login/actions.ts` around lines 14 - 21, Before calling signIn in
app/login/actions.ts, validate the email and rate-limit state (the same checks
performed in sendVerificationRequest in auth.ts) and if validation or rate
limiting fails throw an AuthError subclass (e.g., new AccessDenied("Rate
limited") or new AccessDenied("Failed to send verification email")) so the catch
block (which checks instanceof AuthError) will catch it and redirect to
"/login?error=AccessDenied"; locate the signIn call in signIn("email", { email,
redirectTo: "/" }) and move/email- and rate-limit-validation logic into a
pre-check that throws AccessDenied when appropriate rather than relying on
sendVerificationRequest to surface those errors.

}

export async function signInWithCredentials(formData: FormData) {
const email = formData.get("email") as string;
const password = formData.get("password") as string;

if (!email || !password || !/@iiitl\.ac\.in$/i.test(email)) {
redirect("/login?error=InvalidCredentials")
}
Comment on lines +35 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize email to lowercase before lookup/sign-in.

The credentials provider in auth.ts does findOne({ email: credentials.email }) without normalization. If the stored email is lowercase (from registration) but the user types mixed case here, the lookup fails. Normalize once here before calling signIn:

-    const email = formData.get("email") as string;
+    const email = (formData.get("email") as string | null)?.trim().toLowerCase() ?? "";

Apply the same in signInWithMagicLink and in app/register/actions.ts (before storing / hashing).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/login/actions.ts` around lines 24 - 30, Normalize the email to lowercase
(and trim whitespace) before any lookup or storage to ensure case-insensitive
matching: in signInWithCredentials convert the extracted email to email =
(formData.get("email") as string).trim().toLowerCase() before the
validation/redirect and before calling the auth signIn logic; apply the same
normalization in signInWithMagicLink where the email is read, and in the
register flow in app/register/actions.ts before saving or hashing the email
(e.g., in the function that persists the new user) so stored emails are
consistently lowercase for findOne({ email: ... }) lookups.


try {
await signIn("credentials", { email, password, redirectTo: "/" })
} catch (error) {
if (error instanceof AuthError) {
// AuthError translates to invalid credentials
redirect("/login?error=InvalidCredentials")
}
throw error
}
}

export async function signInWithGoogle() {
try {
await signIn("google", { redirectTo: "/" })
} catch (error) {
if (error instanceof AuthError) {
redirect("/login?error=AccessDenied")
}
throw error
}
}

33 changes: 30 additions & 3 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import { Section } from "@/components/Section";
import { signInWithCredentials, signInWithMagicLink, signInWithGoogle } from "./actions";

export const metadata = { title: "Sign in" };

Expand All @@ -11,27 +12,53 @@
<p className="mt-2 text-sm text-muted">
Sign in to access the alumni directory, events, and the job board.
</p>
<form className="mt-6 space-y-4">
<form action={signInWithCredentials} className="mt-6 space-y-4">
<div>
<label className="text-sm font-medium">Email</label>
<input
type="email"
name="email"
required
className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm"
/>
</div>
<div>
<label className="text-sm font-medium">Password</label>
<input
type="password"
type="password"
name="password"
className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm"
/>
</div>

<button
type="button"
type="submit"
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-brand text-sm font-semibold text-white hover:bg-brand-700"
>
Sign in
</button>

<button
formAction={signInWithMagicLink}
type="submit"
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-transparent border border-brand text-brand text-sm font-semibold hover:bg-brand-50"
>
Send magic link instead
</button>

<div className="flex gap-2 items-center justify-center">
<div className="h-0.5 bg-background-700 w-full"></div>
<div>or</div>
<div className="h-0.5 bg-background-700 w-full"></div>
</div>
<button
formAction={signInWithGoogle}
type="submit"
formNoValidate
className="inline-flex h-11 w-full items-center justify-center rounded-md border border-border bg-background text-sm font-semibold text-white hover:bg-background-700"
>
<span className="h-5 w-5 mr-1"><img src="/google.svg" alt="" className="w-full" /></span>Sign-in with Google

Check warning on line 60 in app/login/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck-and-build

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</form>
<p className="mt-4 text-center text-sm text-muted">
New to IIITL Alumni?{" "}
Expand Down
65 changes: 65 additions & 0 deletions app/register/RegistrationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

// app/register/RegisterForm.tsx
import { useActionState } from "react";
import { handleRegistration, signUpWithGoogle } from "@/app/register/actions";

export default function RegisterForm() {
// useActionState takes your action and an initial state object
const [state, formAction, isPending] = useActionState(handleRegistration, null);

return (
<form action={formAction} className="mt-6 grid gap-4 sm:grid-cols-2">

{/* Display the message from the server action here! */}
{state?.message && (
<div className={`sm:col-span-2 p-3 rounded-md text-sm ${state.success ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}`}>
{state.message}
</div>
)}

<div className="sm:col-span-2">
<label htmlFor="name" className="text-sm font-medium">Full name</label>
<input type="text" id="name" name="name" required className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm" />
</div>
<div className="sm:col-span-2">
<label htmlFor="email" className="text-sm font-medium">IIITL email or roll no.</label>
<input type="email" id="email" name="email" required className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm" />
Comment on lines +26 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the identifier field.

type="email" plus the server-side @iiitl.ac.in check means roll numbers cannot actually be submitted here, so the label is misleading. If roll numbers are intentionally unsupported, make the label email-only; otherwise this field and the server action need to accept a text identifier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/register/RegistrationForm.tsx` around lines 26 - 27, The label/input pair
in RegistrationForm uses a misleading "IIITL email or roll no." label while the
input is <input id="email" name="email" type="email"> and the server-side
validation enforces an `@iiitl.ac.in` address, preventing roll numbers; either
change the label to explicitly say "IIITL email" and keep the input as
type="email", or make the field accept text by changing the input type to "text"
and update the server action that checks the `@iiitl.ac.in` domain to also
accept/validate roll numbers; update references to id="email" and name="email"
accordingly (or rename to identifier if you broaden semantics) so client and
server agree.

</div>
<div className="sm:col-span-2">
<label htmlFor="password" className="text-sm font-medium">Password</label>
<input type="password" id="password" name="password" minLength={8} required className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm" />
</div>
<div>
<label htmlFor="branch" className="text-sm font-medium">Branch</label>
<select id="branch" name="branch" className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm">
<option value="CSE">CSE</option>
<option value="IT">IT</option>
<option value="ECE">ECE</option>
</select>
</div>
<div>
<label htmlFor="graduationYear" className="text-sm font-medium">Graduation year</label>
<input type="number" id="graduationYear" name="graduationYear" required className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm" />
</div>

<div className="sm:col-span-2">
<button type="submit" disabled={isPending} className="inline-flex h-11 w-full items-center justify-center rounded-md bg-brand text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-50">
{isPending ? "Creating..." : "Create account"}
</button>
</div>

<div className="flex gap-2 items-center justify-center w-full sm:col-span-2">
<div className="h-0.5 bg-background-700 w-full"></div>
<div>or</div>
<div className="h-0.5 bg-background-700 w-full"></div>
</div>
<div className="sm:col-span-2">
<button formAction={signUpWithGoogle} formNoValidate type="submit" className="inline-flex h-11 w-full items-center justify-center rounded-md bg-background border border-border text-sm font-semibold text-white hover:bg-background-700">
<span className="h-5 w-5 mr-1"><img src="/google.svg" alt="" className="w-full" /></span>

Check warning on line 59 in app/register/RegistrationForm.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck-and-build

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
Sign-up with Google
</button>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</form>
);
}
109 changes: 109 additions & 0 deletions app/register/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use server"

import { signIn } from "@/auth"
import { AuthError } from "next-auth"
import { redirect } from "next/navigation"
import { headers } from "next/headers"
import bcrypt from "bcryptjs";
import crypto from "crypto";
import { redisClient, limit } from "@/lib/ratelimit";
import { sendEmail } from "@/lib/email";
import clientPromise from "@/lib/mongodb";

export async function handleRegistration(prevState: unknown, formData: FormData) {
try {
const authUrl = process.env.AUTH_URL;
if (!authUrl) throw new Error("Server configuration error: AUTH_URL is missing.");

const rawEmail = formData.get("email");
const rawPassword = formData.get("password");
const rawBranch = formData.get("branch");
const rawGradYear = formData.get("graduationYear");
const rawName = formData.get("name");

// 1. Explicitly narrow formData values to strings and reject empty/files
if (typeof rawEmail !== "string" || !rawEmail.trim()) throw new Error("Invalid email");
if (typeof rawPassword !== "string") throw new Error("Invalid password");
if (typeof rawBranch !== "string" || !rawBranch.trim()) throw new Error("Invalid branch");
if (typeof rawGradYear !== "string" || !rawGradYear.trim()) throw new Error("Invalid graduation year");
if (typeof rawName !== "string" || !rawName.trim()) throw new Error("Invalid name");

const email = rawEmail.trim();
const password = rawPassword;
const branch = rawBranch.trim();
const graduationYearStr = rawGradYear.trim();
const name = rawName.trim();

if (!/@iiitl\.ac\.in$/i.test(email)) throw new Error("Invalid domain. Must end with @iiitl.ac.in");

// 2. Validate password complexity and graduation year
if (password.length < 8) throw new Error("Password must be at least 8 characters long.");

const yearParsed = parseInt(graduationYearStr, 10);
const currentYear = new Date().getFullYear();
// Assuming IIITL started recently, graduation years from ~2018 to few years ahead
if (isNaN(yearParsed) || yearParsed < 2018 || yearParsed > currentYear + 8) {
throw new Error("Invalid graduation year.");
}

// 4. Rate Limiting by IP and Email
const headerList = await headers();
const ip = headerList.get("x-forwarded-for") || "unknown_ip";

// Throttle by IP (prevent spam bots)
const ipLimit = await limit(`verify_ip_${ip}`, { maxRequests: 5, window: "1h" });
if (!ipLimit.success) throw new Error("Too many registration attempts. Please try again later.");

// Throttle by Email (prevent inbox bombing)
const emailLimit = await limit(`verify_email_${email}`, { maxRequests: 3, window: "1h" });
if (!emailLimit.success) throw new Error("Verification emails are being sent too quickly. Please check your inbox or wait an hour.");

// 3. Check existing user in MongoDB
const db = (await clientPromise).db();
const existingUser = await db.collection("users").findOne({ email });
if (existingUser) {
throw new Error("An account is already registered with this email. Please sign in instead.");
}

// 5. Hash the password and create pending user
const hashedPassword = await bcrypt.hash(password, 10);
const token = crypto.randomUUID();

const pendingUser = {
email,
name,
branch,
graduationYear: yearParsed,
hashedPassword,
};

await redisClient?.set(`verify_${token}`, JSON.stringify(pendingUser), { ex: 24*60*60 });
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent failure when Redis is unavailable.

redisClient?.set(...) no-ops when Redis credentials aren't configured, but the function still proceeds to send a verification email containing a token that will never resolve (since app/api/verify-email/route.ts reads from the same Redis). Throw (or return the generic failure) when redisClient is null so users aren't directed to a broken verification link.

+    if (!redisClient) {
+      throw new Error("Verification store unavailable");
+    }
-    await redisClient?.set(`verify_${token}`, JSON.stringify(pendingUser), { ex: 24*60*60 });
+    await redisClient.set(`verify_${token}`, JSON.stringify(pendingUser), { ex: 24 * 60 * 60 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/register/actions.ts` at line 36, The code currently uses an optional
chain on redisClient (redisClient?.set(...)) which silently no-ops when Redis
isn't configured; update the logic in app/register/actions.ts to check
redisClient before attempting to set `verify_${token}` with `pendingUser` and,
if redisClient is null/undefined, immediately throw or return the same generic
failure path used for other storage/email problems so the verification email is
not sent for a token that cannot be validated; reference the `redisClient`
variable, the `set` call for `verify_${token}`, and the `pendingUser` payload to
locate and change the behavior.


const verifyUrl = `${authUrl}/api/verify-email?token=${token}`;

await sendEmail({
to: email,
subject: "Verify your IIITL Alumni Account",
text: `Click here to verify your account: ${verifyUrl}`,
html: `<a href="${verifyUrl}">Verify Account</a>`
});

return { success: true, message: "Check your email to verify your account." };
} catch (error) {
if (error instanceof Error) {
return { success: false, message: error.message };
}
return { success: false, message: "Registration failed." };
}
Comment on lines +92 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not echo raw error messages to the client — violates issue #6 abuse-monitoring requirement.

Issue #6 explicitly states "Failed/rejected sign-up attempts are logged for abuse monitoring but not shown to users." Returning error.message exposes internal failure details (DB errors, Redis errors, stack-trace-adjacent info, etc.) to the client and enables account/email enumeration. There is also no logging of failures for abuse monitoring.

Return a generic message for all failures (including the domain check) and log the real error server-side.

🛡️ Proposed fix
-  } catch (error) {
-    if (error instanceof Error) {
-      return { success: false, message: error.message };
-    }
-    return { success: false, message: "Registration failed." };
-  }
+  } catch (error) {
+    console.error({
+      severity: "WARNING",
+      event: "REGISTRATION_FAILED",
+      error: error instanceof Error ? error.message : String(error),
+    });
+    // Always return a generic message to avoid leaking details / enabling enumeration.
+    return { success: false, message: "Unable to complete registration. Please try again." };
+  }

Consider returning the same generic success-shaped message for the invalid-domain case as well so attackers can’t distinguish rejected domains from accepted ones via response timing/wording.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
if (error instanceof Error) {
return { success: false, message: error.message };
}
return { success: false, message: "Registration failed." };
}
} catch (error) {
console.error({
severity: "WARNING",
event: "REGISTRATION_FAILED",
error: error instanceof Error ? error.message : String(error),
});
// Always return a generic message to avoid leaking details / enabling enumeration.
return { success: false, message: "Unable to complete registration. Please try again." };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/register/actions.ts` around lines 48 - 53, The catch block in the
register action currently returns raw error.message to the client and doesn't
log failures; change it to always return a generic failure payload (e.g., {
success: false, message: "Registration failed." }) for all errors and ensure the
real Error object is logged server‑side for abuse monitoring (use the same
server logger used elsewhere in this file). Also make the invalid-domain branch
return the identical generic response (do not expose domain-specific messages)
so both the domain check and the catch block produce the same client-visible
output while logging the actual reason internally.

}

export async function signUpWithGoogle() {
try {
await signIn("google", { redirectTo: "/" })
} catch (error) {
if (error instanceof AuthError) {
redirect("/login?error=AccessDenied")
}
throw error
}
}
Loading
Loading