Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/api/forgot-password/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";

export async function POST(req: Request) {
try {
const { email } = await req.json();

if (!email) {
return NextResponse.json(
{ message: "Email is required." },
{ status: 400 },
);
}
Comment on lines +7 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add email format validation.

The handler only checks for email presence, not format. Invalid email formats (e.g., "notanemail", "@.com") will pass validation. Additionally, the API doesn't trim whitespace while the client does, which could create inconsistencies if the client-side validation is bypassed.

✉️ Proposed fix with email validation
+const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
 export async function POST(req: Request) {
   try {
-    const { email } = await req.json();
+    const { email: rawEmail } = await req.json();
+    const email = rawEmail?.trim();
 
     if (!email) {
       return NextResponse.json(
         { message: "Email is required." },
         { status: 400 },
       );
     }
+
+    if (!EMAIL_REGEX.test(email)) {
+      return NextResponse.json(
+        { message: "Please enter a valid email address." },
+        { status: 400 },
+      );
+    }
+
     console.log("Password reset requested for:", email);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/forgot-password/route.ts` around lines 7 - 12, The POST handler
currently only checks for presence of email and returns NextResponse.json on
missing email; update the validation to trim surrounding whitespace from the
incoming email and validate its format (e.g., using a simple regex or a shared
validateEmail utility) before proceeding. Locate the email handling in the
route's POST handler (where NextResponse.json is returned on missing email) and
replace the simple presence check with: 1) email = email.trim(), 2) if empty ->
return 400 as before, and 3) if regex/validateEmail fails -> return
NextResponse.json with a 400 and a message like "Invalid email format." Ensure
the same validation logic matches the client-side behavior.

console.log("Password reset requested for:", email);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove PII from logs.

Logging email addresses constitutes a privacy and compliance violation (GDPR, CCPA). Application logs are often aggregated, stored long-term, and accessible to multiple parties, creating an unnecessary exposure of user PII.

🔒 Proposed fix

Either remove the log entirely or use a hashed/anonymized identifier:

-    console.log("Password reset requested for:", email);
+    // Log without PII for observability
+    console.log("Password reset requested");

Or if you need to track requests for debugging, hash the email:

+    const crypto = require('crypto');
+    const emailHash = crypto.createHash('sha256').update(email).digest('hex').substring(0, 8);
-    console.log("Password reset requested for:", email);
+    console.log("Password reset requested, hash:", emailHash);
📝 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
console.log("Password reset requested for:", email);
// Log without PII for observability
console.log("Password reset requested");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/forgot-password/route.ts` at line 13, Remove the plaintext-email
console.log that exposes PII: replace the console.log("Password reset requested
for:", email) in the forgot-password handler with either no log or an anonymized
identifier (e.g., compute a one-way hash of the email using
crypto.createHash('sha256') and log the hash or log only non-PII metadata like
request id or email domain). Update the code near the console.log call (the
console.log invocation and the local variable email in the forgot-password POST
handler) so logs never contain raw email addresses.


return NextResponse.json({
message: "If the email exists, reset link sent.",
});
Comment on lines +15 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Implement actual password reset logic or clarify scope.

The endpoint returns a success message but doesn't generate reset tokens, store them, or send emails. Submitting the form provides user feedback but performs no actual password reset operation.

If this stub is intentional (e.g., split into multiple PRs), consider:

  • Adding a TODO comment documenting the missing implementation
  • Updating the PR description to clarify scope
  • Ensuring the issue tracker reflects that email sending is pending

If this should be complete:

  • Integrate an email service (SendGrid, Resend, Nodemailer, etc.)
  • Generate a cryptographically secure reset token
  • Store the token with expiration (database or cache)
  • Send the reset link via email

Would you like help implementing the complete password reset flow, or should this be tracked separately?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/forgot-password/route.ts` around lines 15 - 17, The endpoint
currently returns a stubbed success response in the forgot-password route and
does not perform any reset-token generation, storage, or email sending; either
implement the complete flow in the request handler in
app/api/forgot-password/route.ts (generate a cryptographically secure token,
persist it with an expiry linked to the user record or a password_resets
table/cache, compose a reset URL, and call your email service client to send the
link), or explicitly mark the handler as a stub by adding a TODO comment and
updating the PR description/issue tracker; locate the handler around the
NextResponse.json(...) return and update the logic there (or add the TODO) so
the repository clearly reflects whether token creation/storage and email
delivery are implemented or postponed.

} catch (err) {
return NextResponse.json(
{ message: "Internal server error." },
{ status: 500 },
);
}
}
64 changes: 42 additions & 22 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ export default function LoginPage() {
setError("Please fill in both email and password.");
return;
}

setLoading(true);
setError(null);

try {
const response = await signIn("credentials", {
email: trimmedEmail,
password: password,
password,
callbackUrl: "/dashboard",
redirect: false,
});
Expand All @@ -47,64 +48,75 @@ export default function LoginPage() {
if (response?.url) {
window.location.href = response.url;
}
} catch (err) {
} catch (error) {
setError("Login failed. Please try again.");
} finally {
setLoading(false);
}
}

function isEmailAndPasswordEmpty() {
return !email.trim().length || !password.trim().length;
}
const isEmailAndPasswordEmpty = () =>
!email.trim().length || !password.trim().length;

return (
<>
<Navbar />

<div className="flex min-h-[calc(100vh-64px)] items-center justify-center px-4">
<div className="w-full max-w-md space-y-3 rounded-xl border bg-background p-6 shadow-sm">
<div className="w-full max-w-md space-y-4 rounded-xl border bg-background p-6 shadow-sm">
{/* HEADER */}
<div className="text-center space-y-1">
<div className="space-y-1 text-center">
<h1 className="text-2xl font-bold">Welcome back</h1>
<p className="text-sm text-muted-foreground">
Login to your LinkID
</p>
</div>

{/* OAUTH */}
{/* OAUTH BUTTONS */}
<div className="space-y-2">
<Button
variant="outline"
className="w-full flex items-center justify-center gap-2"
className="flex w-full items-center justify-center gap-2"
disabled={googleLoading || githubLoading}
onClick={async () => {
setGoogleLoading(true);
try {
await signIn("google", { callbackUrl: "/dashboard" });
await signIn("google", {
callbackUrl: "/dashboard",
});
} finally {
setGoogleLoading(false);
}
}}
>
{googleLoading ? <Spinner className="h-5 w-5" /> : <FcGoogle className="h-5 w-5" />}
{googleLoading ? (
<Spinner className="h-5 w-5" />
) : (
<FcGoogle className="h-5 w-5" />
)}
{googleLoading ? "Connecting..." : "Continue with Google"}
</Button>

<Button
variant="outline"
className="w-full flex items-center justify-center gap-2"
className="flex w-full items-center justify-center gap-2"
disabled={googleLoading || githubLoading}
onClick={async () => {
setGithubLoading(true);
try {
await signIn("github", { callbackUrl: "/dashboard" });
await signIn("github", {
callbackUrl: "/dashboard",
});
} finally {
setGithubLoading(false);
}
}}
>
{githubLoading ? <Spinner className="h-5 w-5" /> : <FaGithub className="h-5 w-5" />}
{githubLoading ? (
<Spinner className="h-5 w-5" />
) : (
<FaGithub className="h-5 w-5" />
)}
{githubLoading ? "Connecting..." : "Continue with GitHub"}
</Button>
</div>
Expand All @@ -116,7 +128,7 @@ export default function LoginPage() {
<div className="h-px w-full bg-border" />
</div>

{/* FORM */}
{/* LOGIN FORM */}
<form
className="space-y-3"
onSubmit={(e) => {
Expand All @@ -143,7 +155,7 @@ export default function LoginPage() {
}}
/>

{/* PASSWORD WITH TOGGLE */}
{/* PASSWORD FIELD */}
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
Expand All @@ -161,7 +173,7 @@ export default function LoginPage() {

<button
type="button"
onClick={() => setShowPassword(!showPassword)}
onClick={() => setShowPassword((prev) => !prev)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={showPassword ? "Hide password" : "Show password"}
>
Expand All @@ -173,10 +185,11 @@ export default function LoginPage() {
</button>
</div>

{/* FORGOT PASSWORD LINK */}
<div className="flex justify-end">
<Link
href="/forgot-password"
className="text-sm text-muted-foreground hover:underline"
href="/password"
className="text-sm text-muted-foreground transition-colors hover:text-foreground hover:underline"
>
Forgot password?
</Link>
Expand All @@ -187,15 +200,22 @@ export default function LoginPage() {
type="submit"
disabled={loading || isEmailAndPasswordEmpty()}
>
{loading ? "Logging in..." : "Login with Email"}
{loading ? (
<>
<Spinner className="mr-2 h-4 w-4" />
Logging in...
</>
) : (
"Login with Email"
)}
</Button>
</form>

{/* FOOTER */}
<p className="text-center text-sm text-muted-foreground">
Dont have an account?{" "}
Don&apos;t have an account?{" "}
<Link href="/register" className="font-medium hover:underline">
Signup
Sign up
</Link>
</p>
</div>
Expand Down
109 changes: 109 additions & 0 deletions app/password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import { FormEvent, useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Navbar } from "../components/Navbar";

export default function ForgotPasswordPage() {
const [email, setEmail] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [message, setMessage] = useState<string>("");
const [error, setError] = useState<string>("");

const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();

if (!email.trim()) {
setError("Please enter your email address.");
return;
}

setLoading(true);
setError("");
setMessage("");

try {
const response = await fetch("/api/forgot-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email.trim(),
}),
});
Comment thread
ArshiBansal marked this conversation as resolved.

const data: { message?: string } = await response.json();

if (!response.ok) {
throw new Error(data.message || "Something went wrong.");
}

setMessage(
"If an account exists with that email, a password reset link has been sent.",
);

setEmail("");
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to send reset email.",
);
} finally {
setLoading(false);
}
};

return (
<>
<Navbar />

<div className="flex min-h-[calc(100vh-64px)] items-center justify-center px-4">
<div className="w-full max-w-md rounded-xl border bg-background p-6 shadow-sm">
<div className="mb-6 text-center">
<h1 className="text-2xl font-bold">Forgot Password</h1>

<p className="mt-2 text-sm text-muted-foreground">
Enter your email address and we'll send you a password reset link.
Comment thread
ArshiBansal marked this conversation as resolved.
</p>
</div>

<form onSubmit={handleSubmit} className="space-y-4">
{error && <p className="text-sm text-red-500">{error}</p>}

{message && <p className="text-sm text-green-600">{message}</p>}

<Input
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError("");
}}
disabled={loading}
/>

<Button
type="submit"
className="w-full"
disabled={loading || !email.trim()}
>
{loading ? "Sending Reset Link..." : "Send Reset Link"}
</Button>
</form>

<div className="mt-4 text-center">
<Link
href="/login"
className="text-sm text-muted-foreground hover:underline"
>
Back to Login
</Link>
</div>
</div>
</div>
</>
);
}