diff --git a/frontend/app/[locale]/forgot-password/page.tsx b/frontend/app/[locale]/forgot-password/page.tsx
index 38dddfc5..90be6e32 100644
--- a/frontend/app/[locale]/forgot-password/page.tsx
+++ b/frontend/app/[locale]/forgot-password/page.tsx
@@ -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(null);
-
- async function onSubmit(e: React.FormEvent) {
- 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 (
-
-
- Forgot password
-
+import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm";
- {submitted ? (
-
-
- If an account for{" "}
- {email} exists, we’ve sent a
- password reset link.
-
-
-
- Please check your inbox and follow the
- instructions to reset your password.
-
-
-
- Back to login
-
-
- ) : (
-
- )}
-
- {!submitted && (
-
- Remembered your password?{" "}
-
- Log in
-
-
- )}
-
- );
+export default function ForgotPasswordPage() {
+ return ;
}
\ No newline at end of file
diff --git a/frontend/app/[locale]/login/page.tsx b/frontend/app/[locale]/login/page.tsx
index 7821c4d7..0f3509c6 100644
--- a/frontend/app/[locale]/login/page.tsx
+++ b/frontend/app/[locale]/login/page.tsx
@@ -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(null);
- const [errorCode, setErrorCode] =
- useState(null);
- const [email, setEmail] = useState("");
- const [verificationSent, setVerificationSent] =
- useState(false);
- const [showPassword, setShowPassword] =
- useState(false);
-
- async function onSubmit(
- e: React.FormEvent
- ) {
- 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 (
-
-
- Log in
-
-
-
-
-
-
-
-
-
- Don’t have an account?{" "}
-
- Sign up
-
-
-
+
);
}
\ No newline at end of file
diff --git a/frontend/app/[locale]/reset-password/page.tsx b/frontend/app/[locale]/reset-password/page.tsx
index 8cc0006a..a9cfbb79 100644
--- a/frontend/app/[locale]/reset-password/page.tsx
+++ b/frontend/app/[locale]/reset-password/page.tsx
@@ -1,126 +1,11 @@
"use client";
-export const dynamic = "force-dynamic";
-
-
-import { Link } from "@/i18n/routing";
-import { useState } from "react";
import { useSearchParams } from "next/navigation";
-import { Button } from "@/components/ui/button";
+import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm";
export default function ResetPasswordPage() {
const searchParams = useSearchParams();
- const token = searchParams.get("token");
-
- const [loading, setLoading] = useState(false);
- const [password, setPassword] = useState("");
- const [confirmed, setConfirmed] = useState(false);
- const [error, setError] = useState(null);
-
- async function onSubmit(e: React.FormEvent) {
- e.preventDefault();
- setError(null);
-
- if (!token) {
- setError("Invalid or missing reset token.");
- return;
- }
-
- setLoading(true);
-
- 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);
- }
- }
-
- if (!token) {
- return (
-
-
- Invalid or missing reset token.
-
-
-
- Back to login
-
-
- );
- }
-
- return (
-
-
- Reset password
-
-
- {confirmed ? (
-
-
Your password has been reset successfully.
-
-
- Go to login
-
-
- ) : (
-
- )}
-
- );
+ return ;
}
\ No newline at end of file
diff --git a/frontend/app/[locale]/signup/page.tsx b/frontend/app/[locale]/signup/page.tsx
index adf141e5..e89d9294 100644
--- a/frontend/app/[locale]/signup/page.tsx
+++ b/frontend/app/[locale]/signup/page.tsx
@@ -1,197 +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";
-
-/**
- * Prevent open redirect vulnerabilities.
- * Allows only safe relative internal paths.
- */
-function isSafeRedirectUrl(url: string): boolean {
- if (!url.startsWith("/")) return false;
- if (url.startsWith("//")) return false;
- if (url.includes("://")) return false;
- return true;
-}
+import { SignupForm } from "@/components/auth/SignupForm";
+import { getSafeRedirect } from "@/lib/auth/safe-redirect";
export default function SignupPage() {
const locale = useLocale();
const searchParams = useSearchParams();
- const returnToParam = searchParams.get("returnTo");
- const returnTo = returnToParam ?? "";
-
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [verificationRequired, setVerificationRequired] =
- useState(false);
- const [email, setEmail] = useState("");
-
- async function onSubmit(
- e: React.FormEvent
- ) {
- e.preventDefault();
- setLoading(true);
- setError(null);
-
- const formData = new FormData(e.currentTarget);
- const emailValue = String(formData.get("email") || "");
- setEmail(emailValue);
-
- let res: Response | undefined;
-
- try {
- res = await fetch("/api/auth/signup", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- name: formData.get("name"),
- email: emailValue,
- password: formData.get("password"),
- }),
- });
-
- const data = await res.json().catch(() => null);
-
- if (!res.ok) {
- setError(
- data?.error ??
- "Failed to sign up. Please try again."
- );
- return;
- }
-
- if (data?.verificationRequired) {
- setVerificationRequired(true);
- return;
- }
-
- const redirectTarget =
- returnTo && isSafeRedirectUrl(returnTo)
- ? returnTo
- : `/${locale}/dashboard`;
-
- window.location.href = redirectTarget;
- } catch {
- setError(
- "Network error. Please check your connection and try again."
- );
- } finally {
- setLoading(false);
- }
- }
+ const returnTo = getSafeRedirect(
+ searchParams.get("returnTo")
+ );
return (
-
-
- Sign up
-
-
- {!verificationRequired && (
- <>
-
-
-
- >
- )}
-
- {verificationRequired ? (
-
-
- We’ve sent a verification email to{" "}
- {email}.
-
-
-
- Please check your inbox and click the
- verification link to activate your account.
-
-
-
- Go to login
-
-
- ) : (
-
- )}
-
- {!verificationRequired && (
-
- Already have an account?{" "}
-
- Log in
-
-
- )}
-
+
);
}
\ No newline at end of file
diff --git a/frontend/components/auth/AuthErrorBanner.tsx b/frontend/components/auth/AuthErrorBanner.tsx
new file mode 100644
index 00000000..74e76c37
--- /dev/null
+++ b/frontend/components/auth/AuthErrorBanner.tsx
@@ -0,0 +1,29 @@
+// import type { ReactNode } from "react";
+
+type AuthErrorBannerProps = {
+ message: string;
+ actionLabel?: string;
+ onAction?: () => void;
+};
+
+export function AuthErrorBanner({
+ message,
+ actionLabel,
+ onAction,
+}: AuthErrorBannerProps) {
+ return (
+
+
{message}
+
+ {actionLabel && onAction && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/AuthProvidersBlock.tsx b/frontend/components/auth/AuthProvidersBlock.tsx
new file mode 100644
index 00000000..e54e3f64
--- /dev/null
+++ b/frontend/components/auth/AuthProvidersBlock.tsx
@@ -0,0 +1,17 @@
+import { OAuthButtons } from "@/components/auth/OAuthButtons";
+
+export function AuthProvidersBlock() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/AuthShell.tsx b/frontend/components/auth/AuthShell.tsx
new file mode 100644
index 00000000..1875133f
--- /dev/null
+++ b/frontend/components/auth/AuthShell.tsx
@@ -0,0 +1,31 @@
+import type { ReactNode } from "react";
+
+type AuthShellProps = {
+ title: string;
+ children: ReactNode;
+ footer?: ReactNode;
+};
+
+export function AuthShell({
+ title,
+ children,
+ footer,
+}: AuthShellProps) {
+ return (
+
+
+ {title}
+
+
+
+ {children}
+
+
+ {footer && (
+
+ {footer}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/AuthSuccessBanner.tsx b/frontend/components/auth/AuthSuccessBanner.tsx
new file mode 100644
index 00000000..5524ce35
--- /dev/null
+++ b/frontend/components/auth/AuthSuccessBanner.tsx
@@ -0,0 +1,23 @@
+import type { ReactNode } from "react";
+
+type AuthSuccessBannerProps = {
+ message: ReactNode;
+ footer?: ReactNode;
+};
+
+export function AuthSuccessBanner({
+ message,
+ footer,
+}: AuthSuccessBannerProps) {
+ return (
+
+
{message}
+
+ {footer && (
+
+ {footer}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/ForgotPasswordForm.tsx b/frontend/components/auth/ForgotPasswordForm.tsx
new file mode 100644
index 00000000..1bf6206e
--- /dev/null
+++ b/frontend/components/auth/ForgotPasswordForm.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { AuthShell } from "@/components/auth/AuthShell";
+import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner";
+import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner";
+import { EmailField } from "@/components/auth/fields/EmailField";
+
+export function ForgotPasswordForm() {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [emailSent, setEmailSent] = useState(false);
+ const [email, setEmail] = useState("");
+
+ async function onSubmit(
+ e: React.FormEvent
+ ) {
+ e.preventDefault();
+ setLoading(true);
+ setError(null);
+
+ const formData = new FormData(e.currentTarget);
+ const emailValue = String(formData.get("email") || "");
+ setEmail(emailValue);
+
+ try {
+ const res = await fetch(
+ "/api/auth/password-reset",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: emailValue }),
+ }
+ );
+
+ if (!res.ok) {
+ setError(
+ "Failed to send reset email. Please try again."
+ );
+ return;
+ }
+
+ setEmailSent(true);
+ } catch {
+ setError(
+ "Network error. Please check your connection."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ {emailSent ? (
+
+
+ We’ve sent a password reset link to{" "}
+ {email}.
+
+
+ Please check your inbox.
+
+ >
+ }
+ />
+ ) : (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/LoginForm.tsx b/frontend/components/auth/LoginForm.tsx
new file mode 100644
index 00000000..d6946a4f
--- /dev/null
+++ b/frontend/components/auth/LoginForm.tsx
@@ -0,0 +1,196 @@
+"use client";
+
+import { useState } from "react";
+import { Link } from "@/i18n/routing";
+import { Button } from "@/components/ui/button";
+import { AuthShell } from "@/components/auth/AuthShell";
+import { AuthProvidersBlock } from "@/components/auth/AuthProvidersBlock";
+import { EmailField } from "@/components/auth/fields/EmailField";
+import { PasswordField } from "@/components/auth/fields/PasswordField";
+import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner";
+import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner";
+
+
+type LoginFormProps = {
+ locale: string;
+ returnTo: string;
+};
+
+export function LoginForm({
+ locale,
+ returnTo,
+}: LoginFormProps) {
+ const [loading, setLoading] = useState(false);
+ const [errorMessage, setErrorMessage] =
+ useState(null);
+ const [errorCode, setErrorCode] =
+ useState(null);
+ const [email, setEmail] = useState("");
+ const [verificationSent, setVerificationSent] =
+ useState(false);
+
+ async function onSubmit(
+ e: React.FormEvent
+ ) {
+ 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;
+ }
+
+ window.location.href =
+ returnTo || `/${locale}/dashboard`;
+ } 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;
+
+ try {
+ const res = await fetch("/api/auth/resend-verification", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email }),
+ });
+
+ const data = await res.json().catch(() => null);
+
+ if (!res.ok) {
+ setErrorCode(data?.code ?? "RESEND_FAILED");
+ setErrorMessage(
+ data?.error ??
+ "Failed to resend verification email. Please try again."
+ );
+ return;
+ }
+
+ setVerificationSent(true);
+ setErrorCode(null);
+ setErrorMessage(null);
+ } catch (err) {
+ console.error("Resend verification failed:", err);
+ setErrorCode("NETWORK_ERROR");
+ setErrorMessage(
+ "Network error. Please check your connection and try again."
+ );
+ }
+ }
+
+ return (
+
+ Don’t have an account?{" "}
+
+ Sign up
+
+
+ }
+ >
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/ResetPasswordForm.tsx b/frontend/components/auth/ResetPasswordForm.tsx
new file mode 100644
index 00000000..17dcd505
--- /dev/null
+++ b/frontend/components/auth/ResetPasswordForm.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { AuthShell } from "@/components/auth/AuthShell";
+import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner";
+import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner";
+import { PasswordField } from "@/components/auth/fields/PasswordField";
+
+type ResetPasswordFormProps = {
+ token: string;
+};
+
+export function ResetPasswordForm({
+ token,
+}: ResetPasswordFormProps) {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(false);
+
+ async function onSubmit(
+ e: React.FormEvent
+ ) {
+ e.preventDefault();
+ setLoading(true);
+ setError(null);
+
+ const formData = new FormData(e.currentTarget);
+
+ try {
+ const res = await fetch(
+ "/api/auth/password-reset/confirm",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ token,
+ password: formData.get("password"),
+ }),
+ }
+ );
+
+ if (!res.ok) {
+ setError(
+ "Failed to reset password. The link may be invalid or expired."
+ );
+ return;
+ }
+
+ setSuccess(true);
+ } catch {
+ setError(
+ "Network error. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ {success ? (
+
+ ) : (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/SignupForm.tsx b/frontend/components/auth/SignupForm.tsx
new file mode 100644
index 00000000..96dc4d46
--- /dev/null
+++ b/frontend/components/auth/SignupForm.tsx
@@ -0,0 +1,158 @@
+"use client";
+
+import { useState } from "react";
+import { Link } from "@/i18n/routing";
+import { Button } from "@/components/ui/button";
+import { AuthShell } from "@/components/auth/AuthShell";
+import { AuthProvidersBlock } from "@/components/auth/AuthProvidersBlock";
+import { EmailField } from "@/components/auth/fields/EmailField";
+import { PasswordField } from "@/components/auth/fields/PasswordField";
+import { NameField } from "@/components/auth/fields/NameField";
+import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner";
+import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner";
+
+type SignupFormProps = {
+ locale: string;
+ returnTo: string;
+};
+
+export function SignupForm({
+ locale,
+ returnTo,
+}: SignupFormProps) {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] =
+ useState(null);
+ const [verificationRequired, setVerificationRequired] =
+ useState(false);
+ const [email, setEmail] = useState("");
+
+ async function onSubmit(
+ e: React.FormEvent
+ ) {
+ e.preventDefault();
+ setLoading(true);
+ setError(null);
+
+ const formData = new FormData(e.currentTarget);
+ const emailValue = String(formData.get("email") || "");
+ setEmail(emailValue);
+
+ try {
+ const res = await fetch("/api/auth/signup", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ name: formData.get("name"),
+ email: emailValue,
+ password: formData.get("password"),
+ }),
+ });
+
+ const data = await res.json().catch(() => null);
+
+ if (!res.ok) {
+ setError(
+ data?.error ??
+ "Failed to sign up. Please try again."
+ );
+ return;
+ }
+
+ if (data?.verificationRequired) {
+ setVerificationRequired(true);
+ return;
+ }
+
+ window.location.href =
+ returnTo || `/${locale}/dashboard`;
+ } catch {
+ setError(
+ "Network error. Please check your connection and try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ Already have an account?{" "}
+
+ Log in
+
+
+ )
+ }
+ >
+ {!verificationRequired && (
+
+ )}
+
+ {verificationRequired ? (
+
+
+ We’ve sent a verification email to{" "}
+ {email}.
+
+
+
+ Please check your inbox and click the
+ verification link to activate your account.
+
+ >
+ }
+ footer={
+
+ Go to login
+
+ }
+ />
+ ) : (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/fields/EmailField.tsx b/frontend/components/auth/fields/EmailField.tsx
new file mode 100644
index 00000000..86fe4ae0
--- /dev/null
+++ b/frontend/components/auth/fields/EmailField.tsx
@@ -0,0 +1,22 @@
+type EmailFieldProps = {
+ onChange?: (value: string) => void;
+};
+
+export function EmailField({
+ onChange,
+}: EmailFieldProps) {
+ return (
+ onChange(e.target.value)
+ : undefined
+ }
+ />
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/fields/NameField.tsx b/frontend/components/auth/fields/NameField.tsx
new file mode 100644
index 00000000..db84309c
--- /dev/null
+++ b/frontend/components/auth/fields/NameField.tsx
@@ -0,0 +1,19 @@
+type NameFieldProps = {
+ name?: string;
+ placeholder?: string;
+};
+
+export function NameField({
+ name = "name",
+ placeholder = "Name",
+}: NameFieldProps) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth/fields/PasswordField.tsx b/frontend/components/auth/fields/PasswordField.tsx
new file mode 100644
index 00000000..42a5743f
--- /dev/null
+++ b/frontend/components/auth/fields/PasswordField.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { useState } from "react";
+
+type PasswordFieldProps = {
+ name?: string;
+ placeholder?: string;
+ minLength?: number;
+};
+
+export function PasswordField({
+ name = "password",
+ placeholder = "Password",
+ minLength,
+}: PasswordFieldProps) {
+ const [visible, setVisible] = useState(false);
+
+ return (
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/lib/auth/safe-redirect.ts b/frontend/lib/auth/safe-redirect.ts
new file mode 100644
index 00000000..e23a8b11
--- /dev/null
+++ b/frontend/lib/auth/safe-redirect.ts
@@ -0,0 +1,13 @@
+export function getSafeRedirect(
+ raw: string | null | undefined
+): string {
+ if (!raw) return "";
+
+ if (raw.includes("\\")) return "";
+
+ if (!raw.startsWith("/")) return "";
+ if (raw.startsWith("//")) return "";
+ if (raw.includes("://")) return "";
+
+ return raw;
+}
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 3c41f9d1..06157ba8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -7512,7 +7512,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
diff --git a/frontend/project-structure.txt b/frontend/project-structure.txt
index df3e5f36..d0972d47 100644
--- a/frontend/project-structure.txt
+++ b/frontend/project-structure.txt
@@ -70,14 +70,15 @@
📄 OrderStatusAutoRefresh.tsx
📄 page.tsx
📁 orders
- 📄 error.tsx
- 📄 page.tsx
📁 [id]
📄 page.tsx
+ 📄 error.tsx
+ 📄 page.tsx
📄 page.tsx
📁 products
📁 [slug]
📄 page.tsx
+ 📄 page.tsx
📁 signup
📄 page.tsx
📁 terms-of-service
@@ -167,6 +168,7 @@
📄 StatsSection.tsx
📁 auth
📄 OAuthButtons.tsx
+ 📄 PostAuthQuizSync.tsx
📄 ProviderButton.tsx
📁 icons
📄 GitHubIcon.tsx
@@ -347,11 +349,13 @@
📄 0001_add_payment_attempts.sql
📄 0002_clean_martin_li.sql
📄 0003_add_stripe_events_claim_lock.sql
+ 📄 0004_add_api_rate_limits.sql
📁 meta
📄 0000_snapshot.json
📄 0001_snapshot.json
📄 0002_snapshot.json
📄 0003_snapshot.json
+ 📄 0004_snapshot.json
📄 _journal.json
📄 drizzle.config.ts
📄 eslint.config.mjs
@@ -408,6 +412,7 @@
📁 security
📄 admin-csrf.ts
📄 csrf.ts
+ 📄 rate-limit.ts
📁 services
📄 errors.ts
📄 inventory.ts