Skip to content

Add auth sign-in & sign-up for iiitl.ac.in domain#49

Open
Ewan-Dkhar wants to merge 8 commits into
iiitl:mainfrom
Ewan-Dkhar:no-type/authentication
Open

Add auth sign-in & sign-up for iiitl.ac.in domain#49
Ewan-Dkhar wants to merge 8 commits into
iiitl:mainfrom
Ewan-Dkhar:no-type/authentication

Conversation

@Ewan-Dkhar
Copy link
Copy Markdown
Contributor

@Ewan-Dkhar Ewan-Dkhar commented Apr 17, 2026

Description

Added google oauth, /login form submission and magic links for sign-in, google oauth and form /register submmision for sign-up. Create an atomic flow for google oauth sign-up where the user is prompted for the password and if password is failed to set, the user doesn't get register.

Closes #6


Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactor / code style

Checklist

  • I have read CONTRIBUTING.md
  • My branch follows the naming convention (feat/, fix/, docs/, etc.)
  • My commits follow the Conventional Commits format
  • I have run pnpm lint and there are no errors
  • I have tested my changes locally (pnpm dev)
  • I have updated documentation where necessary
  • New and existing UI uses design-system tokens (no arbitrary hex values)
  • I have linked the related issue above

Screenshots / Screen Recording

Before After
image image
image image

Summary by CodeRabbit

  • New Features

    • Multi-method authentication: email/password, magic link, and Google sign-in (with Google setup flow).
    • Registration with email verification and a verification-driven account creation flow.
    • Complete Sign Up page to set a password for Google-based signups.
    • Login and registration UIs updated with dedicated buttons for magic-link and Google flows.
  • Style

    • Added a new theme color for improved dark-mode background.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

@Ewan-Dkhar is attempting to deploy a commit to the mrimmortal09's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Walkthrough

This PR adds a complete NextAuth-based authentication system: Google OAuth, email magic links, and credentials; registration with email verification via Redis; Google-first-time signup requiring password setup; MongoDB persistence and Redis-backed rate limiting; and related UI/server actions and config changes.

Changes

Cohort / File(s) Summary
Auth core & config
auth.ts
Adds NextAuth configuration with MongoDB adapter, Google/Email/Credentials providers, signIn callback enforcing @iiitl.ac.in, Google pending-signup flow, and exported handlers/signIn/signOut/auth.
API routes
app/api/auth/[...nextauth]/route.ts, app/api/verify-email/route.ts
Adds NextAuth route re-exports (GET/POST) and email verification endpoint that validates Redis token, upserts user in MongoDB, deletes token, and redirects.
Login UI & actions
app/login/page.tsx, app/login/actions.ts
Wires login page to server actions for credentials, magic link, and Google; validates domain, applies rate limit for magic links, and calls NextAuth signIn.
Registration UI & actions
app/register/page.tsx, app/register/RegistrationForm.tsx, app/register/actions.ts
Introduces RegisterForm component and handleRegistration server action: validation, bcrypt hashing, Redis pending-store (24h), rate limits, and sending verification email. Includes signUpWithGoogle action.
Google password setup
app/setup-password/page.tsx, app/setup-password/actions.ts
Adds setup-password page that binds token to server action and completeGoogleSignup action to consume Redis pending_google token, create user and accounts docs, hash password, delete token, and redirect.
Database helpers
lib/db.ts, lib/mongodb.ts
Reads MONGODB_URI at call-time in connectDB; adds lib/mongodb.ts exporting clientPromise resolving to the underlying MongoDB client.
Rate limiting infra
lib/ratelimit.ts
Makes redisClient exported for reuse by other modules (used for pending tokens and rate limiting).
Env & deps
.env.example, package.json
Adds AUTH_SECRET, AUTH_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET to env example; adds next-auth, @auth/mongodb-adapter, bcryptjs, types, and pnpm override for mongodb.
Styling
app/globals.css
Adds --color-background-700 and dark-mode --background-700: #1d2846``.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant NextAuth
    participant Redis
    participant MongoDB
    participant EmailService

    User->>Browser: Request magic link (email)
    Browser->>NextAuth: POST /api/auth/signin (email)
    NextAuth->>Redis: rate limit check (magic_link_<email>)
    Redis-->>NextAuth: allowed
    NextAuth->>Redis: store verify_<token> (pending user, 24h)
    NextAuth->>EmailService: send verification email with link
    EmailService-->>NextAuth: email sent
    NextAuth-->>Browser: instruct to check email

    User->>Browser: Click verification link
    Browser->>NextAuth: GET /api/verify-email?token=xyz
    NextAuth->>Redis: GET verify_xyz
    Redis-->>NextAuth: pending user data
    NextAuth->>MongoDB: upsert users by email (insert on new, set emailVerified)
    MongoDB-->>NextAuth: upsert result
    NextAuth->>Redis: DEL verify_xyz
    NextAuth-->>Browser: Redirect /login?success=EmailVerified
Loading
sequenceDiagram
    actor User
    participant Browser
    participant NextAuth
    participant GoogleOAuth
    participant Redis
    participant MongoDB

    User->>Browser: Click "Sign in with Google"
    Browser->>NextAuth: Start OAuth flow
    NextAuth->>GoogleOAuth: Request auth (require hd=iiitl.ac.in)
    GoogleOAuth-->>NextAuth: Return profile (first-time)
    NextAuth->>MongoDB: lookup by google ID/email
    MongoDB-->>NextAuth: not found
    NextAuth->>Redis: store pending_google_<token> (profile, 1h)
    NextAuth-->>Browser: Redirect /setup-password?token=token

    User->>Browser: Submit password on setup page
    Browser->>NextAuth: POST /setup-password (token, password)
    NextAuth->>Redis: GET pending_google_<token>
    Redis-->>NextAuth: google profile data
    NextAuth->>NextAuth: hash password (bcryptjs)
    NextAuth->>MongoDB: insert users doc (hashedPassword, emailVerified)
    NextAuth->>MongoDB: insert accounts doc (google provider link)
    NextAuth->>Redis: DEL pending_google_<token>
    NextAuth-->>Browser: Redirect /login?success=AccountCreated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested labels

accepted-60, special-15

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add auth sign-in & sign-up for iiitl.ac.in domain' accurately describes the main changes—implementing authentication with sign-in and sign-up restricted to the IIITL domain.
Linked Issues check ✅ Passed The PR implements all major requirements from issue #6: NextAuth v5 with MongoDB adapter, email domain validation (@iiitl.ac.in), Google OAuth with hosted-domain checks, DB-stored sessions, wired /login and /register flows, rate-limited magic links (5/hour), and atomic Google signup with password creation.
Out of Scope Changes check ✅ Passed All changes are directly related to authentication implementation: new auth routes, login/register pages with server actions, NextAuth configuration, password hashing, email verification, and supporting utilities. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/globals.css (1)

16-47: ⚠️ Potential issue | 🟡 Minor

Missing light-mode value for --background-700.

--color-background-700 maps to --background-700, but --background-700 is only defined inside @media (prefers-color-scheme: dark). In the default (light) color scheme it is undefined, so bg-background-700 (used in app/login/page.tsx for the "or" dividers and the Google button hover) will resolve to an invalid value and render as transparent/broken for light-mode users.

Define a light-mode counterpart alongside the other :root tokens.

🎨 Proposed fix
 :root {
   --background: `#ffffff`;
+  --background-700: `#e5e7eb`; /* pick an appropriate light-mode shade */
   --foreground: `#0b1220`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/globals.css` around lines 16 - 47, The CSS defines --color-background-700
which maps to --background-700, but --background-700 is only set inside `@media`
(prefers-color-scheme: dark), leaving light mode undefined; add a light-mode
value for --background-700 in the default :root (alongside --background,
--foreground, --muted, etc.) so --color-background-700 resolves in light mode
(this fixes broken bg-background-700 usage such as in app/login/page.tsx for the
"or" dividers and Google button hover).
🧹 Nitpick comments (5)
auth.ts (1)

10-16: Add secret, trustHost, and pages for production hardening.

NextAuth v5 requires AUTH_SECRET in env; making it explicit and configuring pages: { signIn: "/login", error: "/login" } avoids the built-in fallback UI and keeps branding consistent. Also consider trustHost: true if deployed behind a proxy that rewrites Host.

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

In `@auth.ts` around lines 10 - 16, Update the NextAuth initialization (exported
symbols handlers, signIn, signOut, auth) to include the production hardening
options: set secret to process.env.AUTH_SECRET (ensure AUTH_SECRET is provided),
add trustHost: true when behind a proxy, and add pages: { signIn: "/login",
error: "/login" } to override the default UI; place these options inside the
NextAuth({ ... }) call alongside the existing adapter and session config.
app/register/RegistrationForm.tsx (1)

59-59: Prefer next/image over <img>.

Same lint warning as on the login page. Use next/image or an eslint-disable-next-line if intentional.

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

In `@app/register/RegistrationForm.tsx` at line 59, Replace the plain <img>
element in RegistrationForm (the span containing <img src="/google.svg" ... />)
with Next.js's Image component: import Image from 'next/image' at the top of the
file, then render <Image src="/google.svg" alt="Google logo" width={20}
height={20} /> (or appropriate width/height matching the h-5 w-5 container and
keep the surrounding span and classes). If using next/image is intentionally
undesirable, add a clear comment and an eslint-disable-next-line for the
specific rule instead of leaving the plain <img>.
app/setup-password/page.tsx (1)

6-21: Minor: prevent search indexing / referrer leakage of the token.

The pending-signup token is a one-time secret in the URL. Consider adding robots: { index: false } in metadata and setting <meta name="referrer" content="no-referrer" /> (or a route-level referrer-policy header) so a misconfigured outbound link or analytics pixel does not leak the token.

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

In `@app/setup-password/page.tsx` around lines 6 - 21, Add privacy metadata and
referrer policy to the SetupPasswordPage to prevent the one-time token from
being indexed or leaked: export a metadata object containing robots: { index:
false } and include a <meta name="referrer" content="no-referrer" /> in the page
head (or set a route-level referrer-policy header) so the token bound to
actionWithToken/variable token cannot be exposed via search engines or referrer
headers.
app/setup-password/actions.ts (1)

53-54: Consider auto-signing-in the user after account creation.

After the user just completed both Google verification and password creation, redirecting to /login?success=AccountCreated forces another login round trip. Calling signIn("credentials", { email, password, redirectTo: "/" }) here would close the loop and satisfy "registration must be atomic" more naturally.

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

In `@app/setup-password/actions.ts` around lines 53 - 54, Replace the explicit
redirect to "/login?success=AccountCreated" with an automatic sign-in flow:
import and call signIn("credentials", { email, password, redirect: false }) from
next-auth after account creation, check the returned result for success (no
error) and then call redirect("/") on success (or fall back to
redirect("/login?success=AccountCreated") on error); update the code that
currently calls redirect("/login?success=AccountCreated") to perform this
signIn+conditional-redirect using the existing redirect function and the
email/password used in the password creation step.
app/login/page.tsx (1)

59-59: Prefer next/image over <img>.

Lint check lint-and-typecheck-and-build flagged this. Swap to next/image for LCP/bandwidth benefits, or add an ESLint disable if the static SVG is intentional.

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

In `@app/login/page.tsx` at line 59, Lint flagged usage of a raw <img> for the
Google SVG in app/login/page.tsx (the span containing <img src="/google.svg"
...>); replace it with Next's Image component by importing Image from
"next/image" and render <Image src="/google.svg" alt="Google logo" ...> with
explicit width and height (or use priority/unoptimized flags if needed) and
preserve the existing className on the wrapper/span; alternatively, if you
intentionally want a raw <img> for some reason, add the appropriate ESLint
disable comment above the element to satisfy the lint rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/verify-email/route.ts`:
- Around line 20-31: The current findOne + insertOne pair in the verify flow
(db.collection("users").findOne and insertOne) is not atomic and can race;
replace that logic with a single upsert using updateOne({ email:
pendingUser.email }, { $setOnInsert: { ...pendingUser, emailVerified: new
Date(), createdAt: new Date() } }, { upsert: true }) so the insert happens
atomically only if the document doesn't exist, and also ensure a unique index
exists on users.email as a defense-in-depth backstop.

In `@app/login/actions.ts`:
- Around line 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.
- Around line 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.

In `@app/login/page.tsx`:
- Around line 54-60: The Google sign-in button uses
formAction={signInWithGoogle} inside the same form that has a required email
field, causing HTML5 validation to block OAuth; fix by preventing form
validation on that button (add formNoValidate to the button) or move the Google
button outside the form so it doesn't trigger the form's validation; update the
element that renders the Google flow (the <button> using
formAction={signInWithGoogle}) to either include formNoValidate and remove any
name attribute or relocate that button out of the <form> containing the required
email input.

In `@app/register/actions.ts`:
- Around line 11-36: The handleRegistration flow currently accepts
unsafe/missing inputs and is unthrottled; update handleRegistration to (1)
explicitly narrow formData.get(...) results to strings and reject null/File
values for "email","password","name","branch","graduationYear", (2) validate
password (minimum length and simple complexity) before calling bcrypt.hash,
validate name/branch as non-empty strings and parse graduationYear to a number
and ensure it's a 4-digit year within a sane range, (3) check the users DB (the
same collection used by app/api/verify-email/route.ts) for an existing verified
account for the email and return a friendly error instead of sending a new
token, (4) apply rate limiting via limit() on both the requester IP and the
target email to throttle verification-email requests, and (5) ensure AUTH_URL is
validated or derive origin from the incoming request and fail-fast if missing so
the verification link passed to redisClient.set and later used in verify-email
is never malformed.
- 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.
- Around line 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.

In `@app/register/RegistrationForm.tsx`:
- Around line 57-62: The Google OAuth button in RegistrationForm.tsx (the
<button> with formAction={signUpWithGoogle} and type="submit") is triggering
HTML5 validation for required inputs; add the formNoValidate attribute to that
button (or alternatively render it in its own form or change to a non-submit
control that triggers the signUpWithGoogle server action) so clicking "Sign-up
with Google" bypasses the browser-required-field validation and performs the
OAuth flow.

In `@app/setup-password/actions.ts`:
- Around line 9-12: Replace direct throws in the server action in
app/setup-password/actions.ts (where password is read and validated and other
error conditions are thrown) with a returned error state or a redirect so the
client can render inline feedback; specifically, in the function that reads
const password = formData.get("password") as string and in the other throw
sites, return an object like { success: false, error: "Password must be at least
8 characters long." } (to be consumed by useActionState) or perform a redirect
to /setup-password?token=…&error=… instead of throwing Error, ensuring the
action always returns a serializable result rather than throwing to let the UI
re-render with an inline message.
- Around line 18-51: The current flow reads the Redis token with redisClient.get
into rawData then inserts user and account separately which allows races and
orphaned users; change the token claim to an atomic
redisClient.getdel(`pending_google_${token}`) (remove the later
redisClient.del), then before creating a new user query for an existing user by
email (use the same DB via (await clientPromise).db() and check for
existingUser) and either link to that user or fail; perform the user and account
creation inside a MongoDB transaction using clientPromise -> db().startSession()
and session.withTransaction so both inserts (db.collection("users").insertOne
and db.collection("accounts").insertOne) are atomic and rollback on error, and
ensure any errors return/abort cleanly so no orphaned user remains.

In `@auth.ts`:
- Around line 75-91: In authorize(credentials) normalize credentials.email with
toLowerCase().trim() before querying db.collection("users").findOne (and ensure
registration paths also normalize), validate typeof credentials.password ===
"string" and return null if not, perform bcrypt.compare only on a confirmed
string, log failed lookups or bad-password events (do not log the password)
using the existing logger, and return a consistently typed user object (id:
user._id.toString(), email, name) as currently done; reference
functions/objects: authorize, clientPromise, db.collection("users").findOne,
bcrypt.compare.
- Around line 104-124: The current flow generates signupToken and returns
`/setup-password?token=${signupToken}` even when redisClient is missing and also
allows Google sign-ins for an existing credentials-only user which will later
fail with OAuthAccountNotLinked. Fix by: (1) in the sign-in callback where
signupToken is created (symbols: redisClient, signupToken, clientPromise), check
that redisClient is available and the redis set succeeded; if redis is
unavailable, fail fast by logging and returning a redirect with a clear error
(do NOT return the setup-password URL if persistence failed); (2) handle
existingUser explicitly (symbol: existingUser): either implement on-the-fly
linking by inserting an accounts record into the MongoDB "accounts" collection
linking account.providerAccountId to existingUser._id, or reject the sign-in by
returning false/redirecting with an error query like error=OAuthAccountNotLinked
and a user-friendly message instructing them to sign in with credentials first
and link accounts; also ensure completeGoogleSignup reads and validates
signupToken from Redis and returns a clear error when the token is
missing/expired.
- Around line 18-29: The Google provider's `hd` authorization param is only a
hint — update the `signIn` callback (where you currently validate email via
regex) to explicitly check `profile.hd` and only allow sign-ins when `profile.hd
=== 'iiitl.ac.in'`; if the claim is missing or different, return false (or
throw) to reject the sign-in. Locate the Google provider configuration (the
Google({...}) block) and the `signIn` callback that receives `profile`, add the
strict `profile.hd` equality check before any email-based acceptance logic, and
ensure non-matching requests are denied.
- Around line 10-16: The Credentials provider is incompatible with
session.strategy: "database" so update the NextAuth config exported by
NextAuth(...) to resolve this: either change session.strategy to "jwt" and
implement rolling 30-day expiry in the jwt callback (keep session.maxAge:
30*24*60*60 and add refresh logic), or remove the Credentials provider and use
magic-link/OAuth only so database sessions work as intended; as an advanced
alternative you can keep Credentials and manually persist sessions by calling
adapter.createSession() from the jwt callback after authorize() returns (ensure
auth() usage is validated). Locate the NextAuth(...) call and adjust
session.strategy and provider setup accordingly and update related callbacks
(jwt callback or removal of Credentials/authorize) and tests for
signIn/signOut/auth flows.
- Around line 34-67: The sendVerificationRequest handler currently throws plain
Error and uses a per-email rate key; change it to import AccessDenied and
AuthError from "next-auth/errors" and throw new AccessDenied("rate_limited")
when the email limiter fails and throw new AuthError("email_send_failed") on
mailer failure; broaden rate-limiting by adding a second limiter call using the
request IP (e.g., key `magic_link_ip_${ip}` with maxRequests ~10/window '1h') in
addition to the existing `magic_link_${identifier}` check, and address the
unused provider parameter in the function signature by renaming it to _provider
or removing it so linting passes.

In `@package.json`:
- Around line 13-23: The package.json has an incompatibility:
`@auth/mongodb-adapter`@3.11.1 expects mongodb@^6 but mongoose@9.4.1 bundles
mongodb@~7.1 (and you also list mongodb@^7.1.1), causing runtime failure when
adapter receives a v7 MongoClient; fix by reconciling versions in
package.json—either downgrade mongoose to a v8.x line that bundles mongodb@^6
(e.g., change "mongoose" to ^8.x), or upgrade "@auth/mongodb-adapter" to a
release that supports mongodb@^7, or remove the direct "mongodb" dependency so
the install fails early and forces resolution; update the dependency entry for
the chosen package (mongoose or `@auth/mongodb-adapter` or mongodb) and reinstall
and run tests to confirm.

---

Outside diff comments:
In `@app/globals.css`:
- Around line 16-47: The CSS defines --color-background-700 which maps to
--background-700, but --background-700 is only set inside `@media`
(prefers-color-scheme: dark), leaving light mode undefined; add a light-mode
value for --background-700 in the default :root (alongside --background,
--foreground, --muted, etc.) so --color-background-700 resolves in light mode
(this fixes broken bg-background-700 usage such as in app/login/page.tsx for the
"or" dividers and Google button hover).

---

Nitpick comments:
In `@app/login/page.tsx`:
- Line 59: Lint flagged usage of a raw <img> for the Google SVG in
app/login/page.tsx (the span containing <img src="/google.svg" ...>); replace it
with Next's Image component by importing Image from "next/image" and render
<Image src="/google.svg" alt="Google logo" ...> with explicit width and height
(or use priority/unoptimized flags if needed) and preserve the existing
className on the wrapper/span; alternatively, if you intentionally want a raw
<img> for some reason, add the appropriate ESLint disable comment above the
element to satisfy the lint rule.

In `@app/register/RegistrationForm.tsx`:
- Line 59: Replace the plain <img> element in RegistrationForm (the span
containing <img src="/google.svg" ... />) with Next.js's Image component: import
Image from 'next/image' at the top of the file, then render <Image
src="/google.svg" alt="Google logo" width={20} height={20} /> (or appropriate
width/height matching the h-5 w-5 container and keep the surrounding span and
classes). If using next/image is intentionally undesirable, add a clear comment
and an eslint-disable-next-line for the specific rule instead of leaving the
plain <img>.

In `@app/setup-password/actions.ts`:
- Around line 53-54: Replace the explicit redirect to
"/login?success=AccountCreated" with an automatic sign-in flow: import and call
signIn("credentials", { email, password, redirect: false }) from next-auth after
account creation, check the returned result for success (no error) and then call
redirect("/") on success (or fall back to
redirect("/login?success=AccountCreated") on error); update the code that
currently calls redirect("/login?success=AccountCreated") to perform this
signIn+conditional-redirect using the existing redirect function and the
email/password used in the password creation step.

In `@app/setup-password/page.tsx`:
- Around line 6-21: Add privacy metadata and referrer policy to the
SetupPasswordPage to prevent the one-time token from being indexed or leaked:
export a metadata object containing robots: { index: false } and include a <meta
name="referrer" content="no-referrer" /> in the page head (or set a route-level
referrer-policy header) so the token bound to actionWithToken/variable token
cannot be exposed via search engines or referrer headers.

In `@auth.ts`:
- Around line 10-16: Update the NextAuth initialization (exported symbols
handlers, signIn, signOut, auth) to include the production hardening options:
set secret to process.env.AUTH_SECRET (ensure AUTH_SECRET is provided), add
trustHost: true when behind a proxy, and add pages: { signIn: "/login", error:
"/login" } to override the default UI; place these options inside the NextAuth({
... }) call alongside the existing adapter and session config.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b2890d5b-9152-4926-b4ef-f2e46a123d4d

📥 Commits

Reviewing files that changed from the base of the PR and between 282e6de and a3a432b.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/google.svg is excluded by !**/*.svg
📒 Files selected for processing (16)
  • .env.example
  • app/api/auth/[...nextauth]/route.ts
  • app/api/verify-email/route.ts
  • app/globals.css
  • app/login/actions.ts
  • app/login/page.tsx
  • app/register/RegistrationForm.tsx
  • app/register/actions.ts
  • app/register/page.tsx
  • app/setup-password/actions.ts
  • app/setup-password/page.tsx
  • auth.ts
  • lib/db.ts
  • lib/mongodb.ts
  • lib/ratelimit.ts
  • package.json
💤 Files with no reviewable changes (1)
  • lib/db.ts

Comment thread app/api/verify-email/route.ts Outdated
Comment thread app/login/actions.ts
Comment on lines +14 to +21
try {
await signIn("email", { email, redirectTo: "/" })
} catch (error) {
if (error instanceof AuthError) {
redirect("/login?error=AccessDenied")
}
throw error
}
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.

Comment thread app/login/actions.ts
Comment on lines +24 to +30
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")
}
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.

Comment thread app/login/page.tsx
Comment thread app/register/actions.ts
Comment thread auth.ts
Comment thread auth.ts Outdated
Comment on lines +34 to +67
async sendVerificationRequest({ identifier, url, provider }) {
const { success } = await limit(`magic_link_${identifier}`, {
maxRequests: 5,
window: '1h'
});

if(!success) {
console.warn(`Rate limit exceeded for magic link: ${identifier}`);
throw new Error("Rate limit exceeded")
}

try {
await sendEmail({
to: identifier,
subject: "Sign in to the IIITL Platform",
html: `
<div style="font-family: sans-serif; padding: 20px;">
<h2>Welcome to the IIITL Platform</h2>
<p>Click the link below to securely sign in.</p>
<a href="${url}" style="padding: 10px 20px; background-color: #000; color: #fff; text-decoration: none; border-radius: 5px; display: inline-block;">
Sign In
</a>
<p style="margin-top: 20px; font-size: 12px; color: #666;">
If you didn't request this, you can safely ignore this email.
</p>
</div>
`,
text: `Sign in to the IIITL Platform by clicking this link: ${url}`
});
} catch (error) {
console.error("Failed to send magic link via custom mailer:", error);
throw new Error("Failed to send verification email.");
}
},
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

Throw AuthError subclasses so login actions can redirect cleanly; also rate-limit key is too narrow.

Two points:

  • Lines 42/65 throw plain Error(...). app/login/actions.ts:signInWithMagicLink only catches AuthError, so rate-limit and Mailgun failures escape as 500s. Throw new AccessDenied("rate_limited") / new AuthError("email_send_failed") (imported from next-auth/errors) so the existing redirect flow works.
  • The limiter key is magic_link_${identifier}, i.e. per-email only. An attacker enumerating *@iiitl.ac.in can still send N emails per account across all students. Issue #6 is satisfied literally, but consider an additional IP-based bucket (magic_link_ip_${ip} at e.g. 10/h) to cap aggregate abuse.
  • Unused provider parameter at Line 34 (lint hint) — drop it or prefix with _.
NextAuth v5 AccessDenied error class throw sendVerificationRequest
🧰 Tools
🪛 GitHub Check: lint-and-typecheck-and-build

[warning] 34-34:
'provider' is defined but never used

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

In `@auth.ts` around lines 34 - 67, The sendVerificationRequest handler currently
throws plain Error and uses a per-email rate key; change it to import
AccessDenied and AuthError from "next-auth/errors" and throw new
AccessDenied("rate_limited") when the email limiter fails and throw new
AuthError("email_send_failed") on mailer failure; broaden rate-limiting by
adding a second limiter call using the request IP (e.g., key
`magic_link_ip_${ip}` with maxRequests ~10/window '1h') in addition to the
existing `magic_link_${identifier}` check, and address the unused provider
parameter in the function signature by renaming it to _provider or removing it
so linting passes.

Comment thread auth.ts
Comment on lines +75 to +91
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;

const db = (await clientPromise).db();
const user = await db.collection("users").findOne({ email: credentials.email });

// If user doesn't exist or has no password
if (!user || !user.hashedPassword) return null;

const isValid = await bcrypt.compare(credentials.password as string, user.hashedPassword);

if (!isValid) return null;

// Return the user object if password matches
return { id: user._id.toString(), email: user.email, name: user.name };
}
})
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 and constant-time-compare safely; also return a typed user.

  • Line 79: findOne({ email: credentials.email }) is case-sensitive. If registration stored alice@iiitl.ac.in but the user types Alice@iiitl.ac.in here, lookup fails. Normalize with .toLowerCase().trim() before the query (and at write-time in app/register/actions.ts / app/api/verify-email/route.ts).
  • Line 84: credentials.password as string — if a non-string sneaks in via a malformed request this will pass [object Object] into bcrypt.compare. Narrow the type (typeof credentials.password !== "string" → return null).
  • No logging of failed attempts. Issue #6 requires: "Failed/rejected sign-up attempts are logged for abuse monitoring but not shown to users." Log unmatched email / bad password events (without the password) to satisfy this.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@auth.ts` around lines 75 - 91, In authorize(credentials) normalize
credentials.email with toLowerCase().trim() before querying
db.collection("users").findOne (and ensure registration paths also normalize),
validate typeof credentials.password === "string" and return null if not,
perform bcrypt.compare only on a confirmed string, log failed lookups or
bad-password events (do not log the password) using the existing logger, and
return a consistently typed user object (id: user._id.toString(), email, name)
as currently done; reference functions/objects: authorize, clientPromise,
db.collection("users").findOne, bcrypt.compare.

Comment thread auth.ts
Comment thread package.json
@Ewan-Dkhar
Copy link
Copy Markdown
Contributor Author

@MrImmortal09 Could you please review the pr. Would really appreciate feedback on how to improve it.

…de, redis outage handling and credential-only user signing in with google.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
auth.ts (1)

76-87: ⚠️ Potential issue | 🟡 Minor

Normalize credentials email and strictly validate password type before bcrypt compare.

Line 80 uses raw credentials.email (case-sensitive lookup risk), and Line 85 force-casts password (as string) without runtime narrowing.

Proposed change
   async authorize(credentials) {
     if (!credentials?.email || !credentials?.password) return null;
+    if (typeof credentials.password !== "string") return null;
+    const email = String(credentials.email).trim().toLowerCase();

     const db = (await clientPromise).db();
-    const user = await db.collection("users").findOne({ email: credentials.email });
+    const user = await db.collection("users").findOne({ email });

     // If user doesn't exist or has no password 
     if (!user || !user.hashedPassword) return null;

-    const isValid = await bcrypt.compare(credentials.password as string, user.hashedPassword);
+    const isValid = await bcrypt.compare(credentials.password, user.hashedPassword);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@auth.ts` around lines 76 - 87, In the authorize function, normalize
credentials.email (e.g., trim() and toLowerCase()) before using it in
db.collection("users").findOne to avoid case-sensitive lookup misses, and
perform a runtime type check on credentials.password (e.g., ensure typeof
credentials.password === "string") before calling bcrypt.compare instead of
force-casting; update the lookup to use the normalizedEmail variable and only
call bcrypt.compare when the password is a string, returning null otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/verify-email/route.ts`:
- Around line 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.
- Around line 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.

In `@app/register/RegistrationForm.tsx`:
- Around line 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.

In `@auth.ts`:
- Around line 35-63: In sendVerificationRequest, validate that identifier is an
IIITL email (e.g., endsWith or regex for the IIITL domain) before calling
limit/sendEmail and throw an error immediately if it is not allowed; after
calling sendEmail within sendVerificationRequest, check its boolean return value
and throw or propagate an error when sendEmail returns false (so the flow does
not silently succeed), referencing the sendVerificationRequest function and the
sendEmail/identifier symbols to locate the changes.

---

Duplicate comments:
In `@auth.ts`:
- Around line 76-87: In the authorize function, normalize credentials.email
(e.g., trim() and toLowerCase()) before using it in
db.collection("users").findOne to avoid case-sensitive lookup misses, and
perform a runtime type check on credentials.password (e.g., ensure typeof
credentials.password === "string") before calling bcrypt.compare instead of
force-casting; update the lookup to use the normalizedEmail variable and only
call bcrypt.compare when the password is a string, returning null otherwise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e179d617-c74a-4c6e-bf7c-b4ab13df85a3

📥 Commits

Reviewing files that changed from the base of the PR and between a3a432b and 74da903.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • app/api/verify-email/route.ts
  • app/login/actions.ts
  • app/login/page.tsx
  • app/register/RegistrationForm.tsx
  • app/register/actions.ts
  • app/setup-password/actions.ts
  • auth.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/setup-password/actions.ts
  • package.json
  • app/login/actions.ts
  • app/register/actions.ts

Comment on lines +8 to +18
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;
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.

Comment on lines +36 to +37
// 3. Delete the token from Redis
await redisClient.del(`verify_${token}`);
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.

Comment on lines +26 to +27
<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" />
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.

Comment thread auth.ts
Comment on lines +35 to +63
async sendVerificationRequest({ identifier, url }) {
const { success } = await limit(`magic_link_${identifier}`, {
maxRequests: 5,
window: '1h'
});

if(!success) {
console.warn(`Rate limit exceeded for magic link: ${identifier}`);
throw new Error("Rate limit exceeded")
}

try {
await sendEmail({
to: identifier,
subject: "Sign in to the IIITL Platform",
html: `
<div style="font-family: sans-serif; padding: 20px;">
<h2>Welcome to the IIITL Platform</h2>
<p>Click the link below to securely sign in.</p>
<a href="${url}" style="padding: 10px 20px; background-color: #000; color: #fff; text-decoration: none; border-radius: 5px; display: inline-block;">
Sign In
</a>
<p style="margin-top: 20px; font-size: 12px; color: #666;">
If you didn't request this, you can safely ignore this email.
</p>
</div>
`,
text: `Sign in to the IIITL Platform by clicking this link: ${url}`
});
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

Enforce IIITL domain before email send, and fail when mail delivery returns false.

Line 35 currently sends magic links for any identifier, and Lines 47-63 don’t check sendEmail’s boolean result. This can send links to non-eligible domains and report success when delivery actually failed.

Proposed change
   async sendVerificationRequest({ identifier, url }) {
-    const { success } = await limit(`magic_link_${identifier}`, {
+    const normalizedEmail = identifier.trim().toLowerCase();
+    if (!/@iiitl\.ac\.in$/i.test(normalizedEmail)) {
+      console.warn(`Rejected magic link request for non-IIITL email: ${normalizedEmail}`);
+      throw new Error("Invalid email domain");
+    }
+
+    const { success } = await limit(`magic_link_${normalizedEmail}`, {
       maxRequests: 5,
       window: '1h'
     });
...
-    try {
-      await sendEmail({
-        to: identifier,
+    try {
+      const sent = await sendEmail({
+        to: normalizedEmail,
         subject: "Sign in to the IIITL Platform",
         ...
       });
+      if (!sent) {
+        throw new Error("Failed to send verification email.");
+      }
     } catch (error) {
       console.error("Failed to send magic link via custom mailer:", error);
       throw new Error("Failed to send verification email.");
     }
   },
📝 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
async sendVerificationRequest({ identifier, url }) {
const { success } = await limit(`magic_link_${identifier}`, {
maxRequests: 5,
window: '1h'
});
if(!success) {
console.warn(`Rate limit exceeded for magic link: ${identifier}`);
throw new Error("Rate limit exceeded")
}
try {
await sendEmail({
to: identifier,
subject: "Sign in to the IIITL Platform",
html: `
<div style="font-family: sans-serif; padding: 20px;">
<h2>Welcome to the IIITL Platform</h2>
<p>Click the link below to securely sign in.</p>
<a href="${url}" style="padding: 10px 20px; background-color: #000; color: #fff; text-decoration: none; border-radius: 5px; display: inline-block;">
Sign In
</a>
<p style="margin-top: 20px; font-size: 12px; color: #666;">
If you didn't request this, you can safely ignore this email.
</p>
</div>
`,
text: `Sign in to the IIITL Platform by clicking this link: ${url}`
});
async sendVerificationRequest({ identifier, url }) {
const normalizedEmail = identifier.trim().toLowerCase();
if (!/@iiitl\.ac\.in$/i.test(normalizedEmail)) {
console.warn(`Rejected magic link request for non-IIITL email: ${normalizedEmail}`);
throw new Error("Invalid email domain");
}
const { success } = await limit(`magic_link_${normalizedEmail}`, {
maxRequests: 5,
window: '1h'
});
if(!success) {
console.warn(`Rate limit exceeded for magic link: ${normalizedEmail}`);
throw new Error("Rate limit exceeded")
}
try {
const sent = await sendEmail({
to: normalizedEmail,
subject: "Sign in to the IIITL Platform",
html: `
<div style="font-family: sans-serif; padding: 20px;">
<h2>Welcome to the IIITL Platform</h2>
<p>Click the link below to securely sign in.</p>
<a href="${url}" style="padding: 10px 20px; background-color: `#000`; color: `#fff`; text-decoration: none; border-radius: 5px; display: inline-block;">
Sign In
</a>
<p style="margin-top: 20px; font-size: 12px; color: `#666`;">
If you didn't request this, you can safely ignore this email.
</p>
</div>
`,
text: `Sign in to the IIITL Platform by clicking this link: ${url}`
});
if (!sent) {
throw new Error("Failed to send verification email.");
}
} catch (error) {
console.error("Failed to send magic link via custom mailer:", error);
throw new Error("Failed to send verification email.");
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@auth.ts` around lines 35 - 63, In sendVerificationRequest, validate that
identifier is an IIITL email (e.g., endsWith or regex for the IIITL domain)
before calling limit/sendEmail and throw an error immediately if it is not
allowed; after calling sendEmail within sendVerificationRequest, check its
boolean return value and throw or propagate an error when sendEmail returns
false (so the flow does not silently succeed), referencing the
sendVerificationRequest function and the sendEmail/identifier symbols to locate
the changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auth: sign-up & sign-in restricted to @iiitl.ac.in emails

1 participant