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
27 changes: 16 additions & 11 deletions frontend/app/[locale]/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ export default function ForgotPasswordPage() {
setLoading(true);
setError(null);

const res = await fetch("/api/auth/password-reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
try {
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;
}

if (!res.ok) {
setError("Something went wrong. Please try again.");
return;
setSubmitted(true);
} catch (err) {
console.error("Password reset request failed:", err);
setError("Network error. Please check your connection and try again.");
} finally {
setLoading(false);
}

setSubmitted(true);
}

return (
Expand Down
204 changes: 127 additions & 77 deletions frontend/app/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,41 @@ 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 {
getPendingQuizResult,
clearPendingQuizResult,
} from "@/lib/quiz/guest-quiz";
import { Button } from "@/components/ui/button";
import { OAuthButtons } from "@/components/auth/OAuthButtons";

function isSafeRedirectUrl(url: string): boolean {
if (!url.startsWith("/")) return false;
if (url.startsWith("//")) return false;
if (url.includes("://")) return false;
return true;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

const returnToParam = searchParams.get("returnTo");
const returnTo = returnToParam ?? "";

const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [errorCode, setErrorCode] = 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>) {
const [verificationSent, setVerificationSent] =
useState(false);
const [showPassword, setShowPassword] =
useState(false);

async function onSubmit(
e: React.FormEvent<HTMLFormElement>
) {
e.preventDefault();
setLoading(true);
setErrorMessage(null);
Expand All @@ -31,77 +49,90 @@ export default function LoginPage() {
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: emailValue,
password: formData.get("password"),
}),
});
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: emailValue,
password: formData.get("password"),
}),
});

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

if (!res.ok) {
setErrorCode(data?.code ?? null);
if (!res.ok) {
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");
if (data?.code === "EMAIL_NOT_VERIFIED") {
setErrorMessage(
"Your email address is not verified. Please check your inbox."
);
} else {
setErrorMessage("Invalid email or password");
}
return;
}

return;
}

const pendingResult = getPendingQuizResult();

if (pendingResult && data?.userId) {
try {
const quizRes = await fetch("/api/quiz/guest-result", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId: data.userId,
quizId: pendingResult.quizId,
answers: pendingResult.answers,
violations: pendingResult.violations,
timeSpentSeconds: pendingResult.timeSpentSeconds,
}),
});

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

if (pendingResult && data?.userId) {
try {
const quizRes = await fetch("/api/quiz/guest-result", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId: data.userId,
quizId: pendingResult.quizId,
answers: pendingResult.answers,
violations: pendingResult.violations,
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,
})
);
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,
})
);
}
} catch (err) {
console.error("Failed to save quiz result:", err);
} finally {
clearPendingQuizResult();
}
} catch (err) {
console.error("Failed to save quiz result:", err);
} finally {
clearPendingQuizResult();
}

window.location.href = `/${locale}/dashboard`;
return;
const redirectTarget =
returnTo && isSafeRedirectUrl(returnTo)
? returnTo
: `/${locale}/dashboard`;

window.location.href = redirectTarget;
} catch (err) {
console.error("Login request failed:", err);
setErrorMessage(
"Network error. Please check your connection and try again."
);
setErrorCode(null);
} finally {
setLoading(false);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

async function resendVerification() {
Expand All @@ -120,13 +151,17 @@ export default function LoginPage() {

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

<OAuthButtons />

<div className="my-4 flex items-center gap-3">
<div className="h-px flex-1 bg-gray-200" />
<span className="text-xs text-gray-500">or</span>
<span className="text-xs text-gray-500">
or
</span>
<div className="h-px flex-1 bg-gray-200" />
</div>

Expand All @@ -151,8 +186,14 @@ export default function LoginPage() {

<button
type="button"
aria-label={showPassword ? "Hide password" : "Show password"}
onClick={() => setShowPassword(v => !v)}
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"}
Expand All @@ -163,7 +204,9 @@ export default function LoginPage() {
<Link
href={
returnTo
? `/forgot-password?returnTo=${encodeURIComponent(returnTo)}`
? `/forgot-password?returnTo=${encodeURIComponent(
returnTo
)}`
: "/forgot-password"
}
className="text-sm underline text-gray-600"
Expand All @@ -190,11 +233,16 @@ export default function LoginPage() {

{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>
Verification successfully sent to{" "}
<strong>{email}</strong>
</div>
)}

<Button type="submit" disabled={loading} className="w-full">
<Button
type="submit"
disabled={loading}
className="w-full"
>
{loading ? "Logging in..." : "Log in"}
</Button>
</form>
Expand All @@ -204,7 +252,9 @@ export default function LoginPage() {
<Link
href={
returnTo
? `/signup?returnTo=${encodeURIComponent(returnTo)}`
? `/signup?returnTo=${encodeURIComponent(
returnTo
)}`
: "/signup"
}
className="underline"
Expand Down
47 changes: 30 additions & 17 deletions frontend/app/[locale]/reset-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"use client";

export const dynamic = "force-dynamic";
Comment thread
kryvosheyin marked this conversation as resolved.


import { Link } from "@/i18n/routing";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
Expand All @@ -25,23 +28,30 @@ export default function ResetPasswordPage() {

setLoading(true);

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

setLoading(false);

if (!res.ok) {
setError("Invalid or expired reset link.");
return;
try {
const res = await fetch(
"/api/auth/password-reset/confirm",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token,
password,
}),
}
);

if (!res.ok) {
setError("Invalid or expired reset link.");
return;
}

setConfirmed(true);
} catch {
setError("Network error, please try again.");
} finally {
setLoading(false);
}

setConfirmed(true);
}

if (!token) {
Expand All @@ -51,7 +61,10 @@ export default function ResetPasswordPage() {
Invalid or missing reset token.
</div>

<Link href="/login" className="mt-4 inline-block underline">
<Link
href="/login"
className="mt-4 inline-block underline"
>
Back to login
</Link>
</div>
Expand Down
Loading