-
Notifications
You must be signed in to change notification settings - Fork 8
Fixed Issue #6 #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Fixed Issue #6 #44
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
ca0dc14
Added Mongodb.ts for auth setup
2980851
feat: setup Auth.js with MongoDB
bea1245
feat: Implemented email link authentication with domain restriction t…
290cdc6
feat: add Google OAuth provider with domain restriction as well
86b2db1
feat: added a rate limit feature
66b188d
Feat: added logging got auth failures
9ac7f80
Added set password for Google Sign in Users
5e90675
Fixed password authorization and credential login, along with hashing…
e0cf018
Fixed various previous issues with implementation of more clear syste…
d44bc68
fixing stagingUser and createuser adapter override function
b4e4cfc
Fix: rate limit issue
9237b16
Fixing logging issue
36d00bf
added trim().toLowerCase, along with adding domain check to login and…
a4ecc38
Callback url redirection.
7be69d4
Finished setting up a proper access denied page for emails not follow…
04da815
feat: Setup forgot password through link through mail.
b6acefb
Merge branch 'main' into feat/auth-setup
Freny07 560d57f
Fixed dependency repetetion issue.
5ee3d2e
Fixed dependency repetetion issue.
85ab125
Finished setting up a proper access denied page for emails not follow…
e12db39
Fixedissues persisting in pnpm run lint
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ yarn-error.log* | |
|
|
||
| # env files (can opt-in for committing if needed) | ||
| .env* | ||
| .env.local | ||
|
|
||
| # vercel | ||
| .vercel | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| "use client" | ||
|
|
||
| import { useSession } from "next-auth/react" | ||
| import { useRouter } from "next/navigation" | ||
| import { useEffect } from "react" | ||
|
|
||
| export default function HomeWrapper({ children }: { children: React.ReactNode }) { | ||
| const { data: session, status } = useSession() | ||
| const router = useRouter() | ||
|
|
||
| useEffect(() => { | ||
| if (status === "loading") return | ||
|
|
||
| if (session?.user?.needsPassword) { | ||
| router.replace("/set-password?email=" + session.user.email) | ||
| } | ||
| }, [session, status, router]) | ||
|
|
||
| if (status === "loading") { | ||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center"> | ||
| <p className="text-sm text-muted">Loading...</p> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| if (session?.user?.needsPassword) return null | ||
|
|
||
| return <>{children}</> | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| "use client" | ||
|
|
||
| import Link from "next/link" | ||
|
|
||
| export default function AccessDeniedPage() { | ||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center"> | ||
| <div className="mx-auto max-w-md rounded-xl border border-border bg-background p-8 text-center"> | ||
| <h2 className="text-xl font-semibold">Access Denied</h2> | ||
| <p className="mt-3 text-sm text-muted"> | ||
| Only <span className="font-medium text-foreground">@iiitl.ac.in</span> accounts are allowed to access this platform. | ||
| </p> | ||
| <div className="mt-6 flex flex-col gap-3"> | ||
| <Link | ||
| href="/login" | ||
| className="inline-flex h-10 items-center justify-center rounded-md bg-brand px-6 text-sm font-semibold text-white hover:bg-brand-700" | ||
| > | ||
| Back to sign in | ||
| </Link> | ||
| <Link | ||
| href="/register" | ||
| className="inline-flex h-10 items-center justify-center rounded-md bg-brand px-6 text-sm font-semibold text-white hover:bg-brand-700" | ||
| > | ||
| Back to sign up | ||
| </Link> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
|
|
||
|
|
||
| export { GET, POST } from "@/lib/auth" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { NextResponse } from "next/server" | ||
| import { connectDB } from "@/lib/db" | ||
| import { PasswordResetToken, hashToken } from "@/models/PasswordResetToken" | ||
| import clientPromise from "@/lib/mongodb" | ||
| import { Resend } from "resend" | ||
| import crypto from "crypto" | ||
|
|
||
| const resend = new Resend(process.env.RESEND_API_KEY) | ||
|
|
||
| export async function POST(req: Request) { | ||
| try { | ||
| const { email } = await req.json() | ||
| const canonical = email?.trim().toLowerCase() | ||
|
|
||
| if (!canonical || !/@iiitl\.ac\.in$/i.test(canonical)) { | ||
| return NextResponse.json({ error: "Invalid email." }, { status: 400 }) | ||
| } | ||
|
|
||
| const client = await clientPromise | ||
| const db = client.db() | ||
| const user = await db.collection("users").findOne({ email: canonical }) | ||
|
|
||
| if (!user || !user.password) { | ||
| return NextResponse.json({ success: true }) | ||
| } | ||
|
|
||
| await connectDB() | ||
|
|
||
| await PasswordResetToken.deleteOne({ email: canonical }) | ||
|
|
||
| const rawToken = crypto.randomBytes(32).toString("hex") | ||
|
|
||
| await PasswordResetToken.create({ | ||
| email: canonical, | ||
| tokenHash: hashToken(rawToken), | ||
| expiresAt: new Date(Date.now() + 15 * 60 * 1000), | ||
| }) | ||
|
|
||
| // Do NOT log resetUrl — it contains a bearer token that grants account access | ||
| const resetUrl = `${process.env.NEXTAUTH_URL}/reset-password?token=${rawToken}` | ||
|
|
||
| await resend.emails.send({ | ||
| from: process.env.EMAIL_FROM!, | ||
| to: canonical, | ||
| subject: "Reset your IIITL Alumni password", | ||
| html: ` | ||
| <p>You requested a password reset.</p> | ||
| <p>Click the link below to set a new password. This link expires in 15 minutes.</p> | ||
| <a href="${resetUrl}">${resetUrl}</a> | ||
| <p>If you didn't request this, you can ignore this email.</p> | ||
| `, | ||
| }) | ||
|
|
||
| return NextResponse.json({ success: true }) | ||
| } catch (err) { | ||
| console.error("FORGOT PASSWORD ERROR:", err) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { NextResponse } from "next/server" | ||
| import clientPromise from "@/lib/mongodb" | ||
| import bcrypt from "bcryptjs" | ||
| import { cookies } from "next/headers" | ||
| import { randomUUID } from "crypto" | ||
| import { logEvent } from "@/lib/logger" | ||
|
|
||
| export async function POST(req: Request) { | ||
| try { | ||
| const { email, password } = await req.json() | ||
|
|
||
| if (!email || !password) { | ||
| return NextResponse.json({ error: "Missing fields" }, { status: 400 }) | ||
| } | ||
|
|
||
| const canonical = email.trim().toLowerCase() | ||
|
|
||
| if (!/@iiitl\.ac\.in$/i.test(canonical)) { | ||
| await logEvent(canonical, "LOGIN_REJECTED_INVALID_DOMAIN") | ||
| return NextResponse.json({ error: "Invalid credentials." }, { status: 401 }) | ||
| } | ||
|
|
||
| const client = await clientPromise | ||
| const db = client.db() | ||
|
|
||
| const user = await db.collection("users").findOne({ email: canonical }) | ||
| if (!user || !user.password) { | ||
| return NextResponse.json({ error: "Invalid email or password." }, { status: 400 }) | ||
| } | ||
|
|
||
| const isValid = await bcrypt.compare(password, user.password) | ||
| if (!isValid) { | ||
| return NextResponse.json({ error: "Invalid email or password." }, { status: 400 }) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const sessionToken = randomUUID() | ||
| const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) | ||
|
|
||
| await db.collection("sessions").insertOne({ | ||
| sessionToken, | ||
| userId: user._id, | ||
| expires, | ||
| }) | ||
|
|
||
|
|
||
| const useSecureCookies = process.env.NODE_ENV === "production" | ||
| const cookieName = useSecureCookies | ||
| ? "__Secure-next-auth.session-token" | ||
| : "next-auth.session-token" | ||
|
|
||
| const cookieStore = await cookies() | ||
| cookieStore.set(cookieName, sessionToken, { | ||
| httpOnly: true, | ||
| sameSite: "lax", | ||
| secure: useSecureCookies, | ||
| path: "/", | ||
| expires, | ||
| }) | ||
|
|
||
| return NextResponse.json({ success: true }) | ||
|
|
||
| } catch (err: unknown) { | ||
| console.error("Login Error:", err) | ||
| return NextResponse.json({ error: "An internal error occurred. Please try again." }, { status: 500 }) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { NextResponse } from "next/server" | ||
| import { checkRateLimit } from "@/lib/rateLimit" | ||
|
|
||
| export async function POST(req: Request) { | ||
| try { | ||
| const body = await req.json() | ||
| const { email } = body | ||
|
|
||
| if (!email || typeof email !== "string") { | ||
| return NextResponse.json( | ||
| { allowed: false, error: "Invalid email format" }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const isValidDomain = /@iiitl\.ac\.in$/i.test(email) | ||
| if (!isValidDomain) { | ||
| return NextResponse.json( | ||
| { allowed: false, error: "Email must be a valid @iiitl.ac.in domain" }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const result = await checkRateLimit(email) | ||
|
|
||
| return NextResponse.json(result) | ||
| } catch (err) { | ||
| console.error("Rate limit error:", err) | ||
| return NextResponse.json( | ||
| { allowed: false, error: "Malformed request" }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { NextResponse } from "next/server" | ||
| import clientPromise from "@/lib/mongodb" | ||
| import bcrypt from "bcryptjs" | ||
| import { logEvent } from "@/lib/logger" | ||
|
|
||
| export async function POST(req: Request) { | ||
| try { | ||
| const body = await req.json() | ||
| const { name, email, branch, graduationYear, password } = body | ||
|
|
||
| if (!name || !email || !branch || !password) { | ||
| // Log missing-fields rejections so abuse patterns are visible | ||
| await logEvent(email ?? null, "REGISTER_REJECTED_MISSING_FIELDS") | ||
| return NextResponse.json( | ||
| { error: "Missing required fields" }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const canonical = email.trim().toLowerCase() | ||
|
|
||
| if (!/@iiitl\.ac\.in$/i.test(canonical)) { | ||
| await logEvent(canonical, "REGISTER_REJECTED_INVALID_DOMAIN") | ||
| return NextResponse.json( | ||
| { error: "Only @iiitl.ac.in emails are allowed." }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const client = await clientPromise | ||
| const db = client.db() | ||
|
|
||
| const existingUser = await db.collection("users").findOne({ email: canonical }) | ||
| if (existingUser) { | ||
| await logEvent(canonical, "REGISTER_REJECTED_DUPLICATE") | ||
| if (!existingUser.password) { | ||
| return NextResponse.json( | ||
| { error: "Account exists without a password. Please sign in via Magic Link or Google to set one." }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
| return NextResponse.json( | ||
| { error: "An account with this email already exists." }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const hash = await bcrypt.hash(password, 10) | ||
|
|
||
| try { | ||
| await db.collection("users").insertOne({ | ||
| name, | ||
| email: canonical, | ||
| branch, | ||
| graduationYear, | ||
| password: hash, | ||
| createdAt: new Date(), | ||
| }) | ||
| } catch (err: unknown) { | ||
| // Handle the race where two concurrent requests both pass the findOne | ||
| // check above and one then hits a duplicate-key error on insert | ||
| if ( | ||
| typeof err === "object" && | ||
| err !== null && | ||
| "code" in err && | ||
| (err as { code: number }).code === 11000 | ||
| ) { | ||
| await logEvent(canonical, "REGISTER_REJECTED_DUPLICATE") | ||
| return NextResponse.json( | ||
| { error: "An account with this email already exists." }, | ||
| { status: 409 } | ||
| ) | ||
| } | ||
| throw err | ||
| } | ||
|
|
||
| return NextResponse.json({ success: true }) | ||
|
|
||
| } catch (err: unknown) { | ||
| console.error("Registration error:", err) | ||
| return NextResponse.json( | ||
| { error: "Internal Server Error" }, | ||
| { status: 500 } | ||
| ) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { NextResponse } from "next/server" | ||
| import { connectDB } from "@/lib/db" | ||
| import { PasswordResetToken, hashToken } from "@/models/PasswordResetToken" | ||
| import clientPromise from "@/lib/mongodb" | ||
| import bcrypt from "bcryptjs" | ||
|
|
||
| export async function POST(req: Request) { | ||
| try { | ||
| const { token, password } = await req.json() | ||
|
|
||
| if (!token || !password) { | ||
| return NextResponse.json({ error: "Missing fields." }, { status: 400 }) | ||
| } | ||
|
Comment on lines
+11
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enforce password policy in reset endpoint. On Line 11, this route accepts any password value; that allows weaker passwords than registration rules. Add minimum-length validation (and ideally shared policy validation) before hashing. Proposed patch- if (!token || !password) {
+ if (!token || !password) {
return NextResponse.json({ error: "Missing fields." }, { status: 400 })
}
+ if (typeof password !== "string" || password.length < 6) {
+ return NextResponse.json(
+ { error: "Password must be at least 6 characters long." },
+ { status: 400 }
+ )
+ }🤖 Prompt for AI Agents |
||
|
|
||
| await connectDB() | ||
|
|
||
| // findOneAndDelete: atomically consume the token so two concurrent | ||
| // requests cannot both pass and write different passwords | ||
| const record = await PasswordResetToken.findOneAndDelete({ | ||
| tokenHash: hashToken(token), | ||
| expiresAt: { $gt: new Date() }, | ||
| }) | ||
|
|
||
| if (!record) { | ||
| return NextResponse.json( | ||
| { error: "Reset link is invalid or has expired." }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const hashed = await bcrypt.hash(password, 10) | ||
|
|
||
| const client = await clientPromise | ||
| const db = client.db() | ||
|
|
||
| const result = await db | ||
| .collection("users") | ||
| .updateOne({ email: record.email }, { $set: { password: hashed } }) | ||
|
|
||
| // Fail explicitly if no account matched — don't silently return 200 | ||
| if (result.matchedCount === 0) { | ||
| return NextResponse.json({ error: "Account not found." }, { status: 404 }) | ||
| } | ||
|
|
||
| return NextResponse.json({ success: true }) | ||
| } catch (err) { | ||
| console.error("RESET PASSWORD ERROR:", err) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not put user email in the redirect URL.
On Line 15, appending
session.user.emailto the query string exposes PII in logs/history/referrers. Redirect to/set-passwordwithout email and resolve identity from the authenticated session on that page/API.Proposed patch
📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Freny07 note