Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions frontend/app/[locale]/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use client";
import { Link } from "@/i18n/routing";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";

export default function ForgotPasswordPage() {
const searchParams = useSearchParams();
const returnTo = searchParams.get("returnTo");

const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);

async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError(null);

const res = await fetch("/api/auth/password-reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});

setLoading(false);

if (!res.ok) {
setError("Something went wrong. Please try again.");
return;
}

setSubmitted(true);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<div className="mx-auto max-w-sm py-12">
<h1 className="mb-6 text-2xl font-semibold">
Forgot password
</h1>

{submitted ? (
<div className="rounded-md border border-green-400 bg-green-50 p-4 text-sm text-green-800">
<p>
If an account for{" "}
<strong>{email}</strong> exists, we’ve sent a
password reset link.
</p>

<p className="mt-2">
Please check your inbox and follow the
instructions to reset your password.
</p>

<Link
href={
returnTo
? `/login?returnTo=${encodeURIComponent(returnTo)}`
: "/login"
}
className="mt-4 inline-block underline"
>
Back to login
</Link>
Comment on lines +56 to +65

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

🧩 Analysis chain

🏁 Script executed:

# First, find the login page and related files
find frontend -type f -name "*login*" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Search for returnTo usage across the codebase
rg -n "returnTo" --type ts --type tsx frontend/

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Also check if there's any validation or redirect logic in api routes or components
rg -n "redirect\|encodeURI\|pathname" --type ts --type tsx frontend/app/ -A 2 -B 2 | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Search for returnTo without file type restriction
rg -n "returnTo" frontend/

Repository: DevLoversTeam/devlovers.net

Length of output: 1924


🏁 Script executed:

# Find and read the login page
find frontend -type f -name "*login*" -path "*/page.*"

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Search for validation or redirect patterns
rg -n "redirect\|validate\|allowedDomains" frontend/ -A 3 | head -150

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Read the login page to see the returnTo handling
cat -n frontend/app/[locale]/login/page.tsx | sed -n '1,110p'

Repository: DevLoversTeam/devlovers.net

Length of output: 4197


🏁 Script executed:

# Check if there's any validation utility or helper
rg -n "validate\|isValidUrl\|isSafeUrl\|allowed" frontend/ -B 2 -A 2 | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 53


Fix open redirect vulnerability in login redirect logic.

The login page directly redirects to the returnTo parameter without validation at line 104: window.location.href = returnTo || /${locale}/dashboard;. An attacker can craft a link like /login?returnTo=https://evil.com to redirect authenticated users to a malicious site for phishing attacks.

Implement validation to ensure returnTo is a relative path or belongs to an allowed domain before redirecting. Consider using a helper function to validate URLs:

function isSafeRedirectUrl(url: string, locale: string): boolean {
  if (!url) return false;
  // Allow relative paths starting with /
  if (url.startsWith('/')) {
    // Ensure it doesn't contain protocol indicators for external redirects
    return !url.includes('://');
  }
  return false;
}

Then use: window.location.href = isSafeRedirectUrl(returnTo, locale) ? returnTo : /${locale}/dashboard;

</div>
) : (
<form onSubmit={onSubmit} className="space-y-4">
<p className="text-sm text-gray-600">
Enter your email address and we’ll send
you a link to reset your password.
</p>

<input
type="email"
required
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full rounded border px-3 py-2"
/>

{error && (
<p className="text-sm text-red-600">
{error}
</p>
)}

<Button
type="submit"
disabled={loading}
className="w-full"
>
{loading
? "Sending reset link..."
: "Send reset link"}
</Button>
</form>
)}

{!submitted && (
<p className="mt-4 text-sm text-gray-600">
Remembered your password?{" "}
<Link
href={
returnTo
? `/login?returnTo=${encodeURIComponent(returnTo)}`
: "/login"
}
className="underline"
>
Log in
</Link>
</p>
)}
</div>
);
}
147 changes: 119 additions & 28 deletions frontend/app/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,65 @@
"use client";

import { useLocale } from 'next-intl';
import { Link } from '@/i18n/routing';
import { useLocale } from "next-intl";
import { Link } from "@/i18n/routing";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { getPendingQuizResult, clearPendingQuizResult } from "@/lib/quiz/guest-quiz";
import { Button } from "@/components/ui/button";
import { OAuthButtons } from '@/components/auth/OAuthButtons';
import { OAuthButtons } from "@/components/auth/OAuthButtons";

export default function LoginPage() {
const searchParams = useSearchParams();
const returnTo = searchParams.get("returnTo");
const locale = useLocale();

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [errorCode, setErrorCode] = useState<string | null>(null);
const [email, setEmail] = useState("");
const [verificationSent, setVerificationSent] = useState(false);
const [showPassword, setShowPassword] = useState(false);

async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError(null);
setErrorMessage(null);
setErrorCode(null);
setVerificationSent(false);

const formData = new FormData(e.currentTarget);
const emailValue = String(formData.get("email") || "");
setEmail(emailValue);

const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: formData.get("email"),
email: emailValue,
password: formData.get("password"),
}),
});

const data = await res.json().catch(() => null);
setLoading(false);

if (!res.ok) {
setError("Invalid email or password");
setErrorCode(data?.code ?? null);

if (data?.code === "EMAIL_NOT_VERIFIED") {
setErrorMessage(
"Your email address is not verified. Please check your inbox."
);
} else {
setErrorMessage("Invalid email or password");
}

return;
}

const data = await res.json();
const pendingResult = getPendingQuizResult();
if (pendingResult && data.userId) {

if (pendingResult && data?.userId) {
try {
const quizRes = await fetch("/api/quiz/guest-result", {
method: "POST",
Expand All @@ -53,32 +72,52 @@ export default function LoginPage() {
timeSpentSeconds: pendingResult.timeSpentSeconds,
}),
});

if (!quizRes.ok) {
throw new Error(`Failed to save quiz result: ${quizRes.status}`);
}

const result = await quizRes.json();

if (result.success) {
sessionStorage.setItem('quiz_just_saved', JSON.stringify({
score: result.score,
total: result.totalQuestions,
percentage: result.percentage,
pointsAwarded: result.pointsAwarded,
quizSlug: pendingResult.quizSlug,
}));
sessionStorage.setItem(
"quiz_just_saved",
JSON.stringify({
score: result.score,
total: result.totalQuestions,
percentage: result.percentage,
pointsAwarded: result.pointsAwarded,
quizSlug: pendingResult.quizSlug,
})
);
}
} catch (err) {
console.error('Failed to save quiz result:', err);
console.error("Failed to save quiz result:", err);
} finally {
clearPendingQuizResult();
}

window.location.href = `/${locale}/dashboard`;
return;
}

window.location.href = returnTo || `/${locale}/dashboard`;
}

async function resendVerification() {
if (!email) return;

await fetch("/api/auth/resend-verification", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});

setVerificationSent(true);
setErrorCode(null);
setErrorMessage(null);
}

return (
<div className="mx-auto max-w-sm py-12">
<h1 className="mb-6 text-2xl font-semibold">Log in</h1>
Expand All @@ -98,17 +137,62 @@ export default function LoginPage() {
placeholder="Email"
required
className="w-full rounded border px-3 py-2"
onChange={e => setEmail(e.target.value)}
/>

<input
name="password"
type="password"
placeholder="Password"
required
className="w-full rounded border px-3 py-2"
/>

{error && <p className="text-sm text-red-600">{error}</p>}
<div className="relative">
<input
name="password"
type={showPassword ? "text" : "password"}
placeholder="Password"
required
className="w-full rounded border px-3 py-2 pr-10"
/>

<button
type="button"
aria-label={showPassword ? "Hide password" : "Show password"}
onClick={() => setShowPassword(v => !v)}
className="absolute inset-y-0 right-2 flex items-center text-sm text-gray-500"
>
{showPassword ? "Hide" : "Show"}
</button>
</div>

<div className="text-right">
<Link
href={
returnTo
? `/forgot-password?returnTo=${encodeURIComponent(returnTo)}`
: "/forgot-password"
}
className="text-sm underline text-gray-600"
>
Forgot password?
</Link>
</div>

{errorMessage && !verificationSent && (
<div className="rounded-md border border-yellow-400 bg-yellow-50 p-3 text-sm text-yellow-800">
<p>{errorMessage}</p>

{errorCode === "EMAIL_NOT_VERIFIED" && (
<button
type="button"
onClick={resendVerification}
className="mt-2 underline"
>
Resend verification email
</button>
)}
</div>
)}

{verificationSent && (
<div className="rounded-md border border-green-400 bg-green-50 p-3 text-sm text-green-800">
Verification successfully sent to <strong>{email}</strong>
</div>
)}

<Button type="submit" disabled={loading} className="w-full">
{loading ? "Logging in..." : "Log in"}
Expand All @@ -117,10 +201,17 @@ export default function LoginPage() {

<p className="mt-4 text-sm text-gray-600">
Don’t have an account?{" "}
<Link href={returnTo ? `/signup?returnTo=${encodeURIComponent(returnTo)}` : '/signup'} className="underline">
<Link
href={
returnTo
? `/signup?returnTo=${encodeURIComponent(returnTo)}`
: "/signup"
}
className="underline"
>
Sign up
</Link>
</p>
</div>
);
}
}
Loading