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
122 changes: 3 additions & 119 deletions frontend/app/[locale]/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,123 +1,7 @@
"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);

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

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);
}
}

return (
<div className="mx-auto max-w-sm py-12">
<h1 className="mb-6 text-2xl font-semibold">
Forgot password
</h1>
import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm";

{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>
</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>
);
export default function ForgotPasswordPage() {
return <ForgotPasswordForm />;
}
217 changes: 10 additions & 207 deletions frontend/app/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,219 +1,22 @@
"use client";

import { useLocale } from "next-intl";
import { Link } from "@/i18n/routing";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
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;
}
import { LoginForm } from "@/components/auth/LoginForm";
import { getSafeRedirect } from "@/lib/auth/safe-redirect";

export default function LoginPage() {
const searchParams = useSearchParams();
const locale = useLocale();
const searchParams = useSearchParams();

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 [email, setEmail] = useState("");
const [verificationSent, setVerificationSent] =
useState(false);
const [showPassword, setShowPassword] =
useState(false);

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

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

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);

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");
}
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);
}
}

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);
}
const returnTo = getSafeRedirect(
searchParams.get("returnTo")
);

return (
<div className="mx-auto max-w-sm py-12">
<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>
<div className="h-px flex-1 bg-gray-200" />
</div>

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

<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"}
</Button>
</form>

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