Add auth sign-in & sign-up for iiitl.ac.in domain#49
Conversation
|
@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. |
WalkthroughThis 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorMissing light-mode value for
--background-700.
--color-background-700maps to--background-700, but--background-700is only defined inside@media (prefers-color-scheme: dark). In the default (light) color scheme it is undefined, sobg-background-700(used inapp/login/page.tsxfor 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
:roottokens.🎨 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: Addsecret,trustHost, andpagesfor production hardening.NextAuth v5 requires
AUTH_SECRETin env; making it explicit and configuringpages: { signIn: "/login", error: "/login" }avoids the built-in fallback UI and keeps branding consistent. Also considertrustHost: trueif deployed behind a proxy that rewritesHost.🤖 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: Prefernext/imageover<img>.Same lint warning as on the login page. Use
next/imageor aneslint-disable-next-lineif 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
tokenis a one-time secret in the URL. Consider addingrobots: { index: false }inmetadataand setting<meta name="referrer" content="no-referrer" />(or a route-levelreferrer-policyheader) 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=AccountCreatedforces another login round trip. CallingsignIn("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: Prefernext/imageover<img>.Lint check
lint-and-typecheck-and-buildflagged this. Swap tonext/imagefor 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
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/google.svgis excluded by!**/*.svg
📒 Files selected for processing (16)
.env.exampleapp/api/auth/[...nextauth]/route.tsapp/api/verify-email/route.tsapp/globals.cssapp/login/actions.tsapp/login/page.tsxapp/register/RegistrationForm.tsxapp/register/actions.tsapp/register/page.tsxapp/setup-password/actions.tsapp/setup-password/page.tsxauth.tslib/db.tslib/mongodb.tslib/ratelimit.tspackage.json
💤 Files with no reviewable changes (1)
- lib/db.ts
| try { | ||
| await signIn("email", { email, redirectTo: "/" }) | ||
| } catch (error) { | ||
| if (error instanceof AuthError) { | ||
| redirect("/login?error=AccessDenied") | ||
| } | ||
| throw error | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: sendVerificationRequest exceptions not caught, and return to client is ok nextauthjs/next-auth#13124
- 2: https://authjs.dev/reference/core/errors
- 3: https://authjs.dev/guides/providers/email-http
- 4: https://authjs.dev/reference/core/providers_email
🏁 Script executed:
find . -name "auth.ts" -o -name "auth.config.ts" | head -20Repository: iiitl/alumni
Length of output: 65
🏁 Script executed:
find . -name "actions.ts" | grep -i loginRepository: iiitl/alumni
Length of output: 78
🏁 Script executed:
cat -n ./auth.tsRepository: iiitl/alumni
Length of output: 5392
🏁 Script executed:
cat -n ./app/login/actions.tsRepository: 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") | ||
| } |
There was a problem hiding this comment.
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.
| 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."); | ||
| } | ||
| }, |
There was a problem hiding this comment.
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:signInWithMagicLinkonly catchesAuthError, so rate-limit and Mailgun failures escape as 500s. Thrownew AccessDenied("rate_limited")/new AuthError("email_send_failed")(imported fromnext-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.incan still send N emails per account across all students. Issue#6is satisfied literally, but consider an additional IP-based bucket (magic_link_ip_${ip}at e.g. 10/h) to cap aggregate abuse. - Unused
providerparameter 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.
| 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 }; | ||
| } | ||
| }) |
There was a problem hiding this comment.
Normalize email and constant-time-compare safely; also return a typed user.
- Line 79:
findOne({ email: credentials.email })is case-sensitive. If registration storedalice@iiitl.ac.inbut the user typesAlice@iiitl.ac.inhere, lookup fails. Normalize with.toLowerCase().trim()before the query (and at write-time inapp/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]intobcrypt.compare. Narrow the type (typeof credentials.password !== "string"→ return null). - No logging of failed attempts. Issue
#6requires: "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.
|
@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.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
auth.ts (1)
76-87:⚠️ Potential issue | 🟡 MinorNormalize 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
app/api/verify-email/route.tsapp/login/actions.tsapp/login/page.tsxapp/register/RegistrationForm.tsxapp/register/actions.tsapp/setup-password/actions.tsauth.tspackage.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
| 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; |
There was a problem hiding this comment.
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.
| 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.
| // 3. Delete the token from Redis | ||
| await redisClient.del(`verify_${token}`); |
There was a problem hiding this comment.
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.
| // 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.
| <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" /> |
There was a problem hiding this comment.
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.
| 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}` | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
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
Checklist
feat/,fix/,docs/, etc.)pnpm lintand there are no errorspnpm dev)Screenshots / Screen Recording
Summary by CodeRabbit
New Features
Style