diff --git a/app/(auth)/forgot-password/page.tsx b/app/(auth)/forgot-password/page.tsx new file mode 100644 index 000000000..2965abf95 --- /dev/null +++ b/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { z } from "zod"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; + +export default function ForgotPasswordPage() { + const [email, setEmail] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + + const emailSchema = z.string().email("Invalid email address").toLowerCase(); + const emailValidation = emailSchema.safeParse(email); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!emailValidation.success) { + toast.error(emailValidation.error.errors[0].message); + return; + } + + setIsLoading(true); + + try { + const response = await fetch("/api/auth/forgot-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: emailValidation.data }), + }); + + const data = await response.json(); + + if (response.ok) { + setIsSubmitted(true); + toast.success("If an account with this email exists, you will receive a password reset email."); + } else { + toast.error(data.error || "Something went wrong"); + } + } catch (error) { + toast.error("Network error. Please try again."); + } finally { + setIsLoading(false); + } + }; + + if (isSubmitted) { + return ( +
+
+
+
+ Papermark Logo +
+

+ Check your email +

+

+ If an account with the email {email} exists, we've sent you a password reset link. +

+

+ The link will expire in 15 minutes for security reasons. +

+
+ + Back to sign in + +
+
+
+
+ ); + } + + return ( +
+
+
+
+ Papermark Logo +
+

+ Forgot your password? +

+

+ Enter your email address and we'll send you a link to reset your password. +

+
+ +
+
+ + setEmail(e.target.value)} + disabled={isLoading} + className={cn( + "border-2", + email.length > 0 && !emailValidation.success ? "border-red-500" : "" + )} + /> +
+ + + +
+ + Back to sign in + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/(auth)/login/page-client.tsx b/app/(auth)/login/page-client.tsx index 28eca7bb3..8b050685d 100644 --- a/app/(auth)/login/page-client.tsx +++ b/app/(auth)/login/page-client.tsx @@ -11,6 +11,8 @@ import { toast } from "sonner"; import { z } from "zod"; import { cn } from "@/lib/utils"; +import Eye from "@/components/shared/icons/eye"; +import EyeOff from "@/components/shared/icons/eye-off"; import { LastUsed, useLastUsed } from "@/components/hooks/useLastUsed"; import Google from "@/components/shared/icons/google"; @@ -25,15 +27,18 @@ export default function Login() { const { next } = useParams as { next?: string }; const [lastUsed, setLastUsed] = useLastUsed(); - const authMethods = ["google", "email", "linkedin", "passkey"] as const; + const authMethods = ["password", "google", "email", "linkedin", "passkey"] as const; type AuthMethod = (typeof authMethods)[number]; const [clickedMethod, setClickedMethod] = useState( undefined, ); const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); const [emailButtonText, setEmailButtonText] = useState( "Continue with Email", ); + const [showPassword, setShowPassword] = useState(false); + const [isPasswordMode, setIsPasswordMode] = useState(false); const emailSchema = z .string() @@ -77,23 +82,46 @@ export default function Login() { return; } - setClickedMethod("email"); - signIn("email", { - email: emailValidation.data, - redirect: false, - ...(next && next.length > 0 ? { callbackUrl: next } : {}), - }).then((res) => { - if (res?.ok && !res?.error) { - setEmail(""); - setLastUsed("credentials"); - setEmailButtonText("Email sent - check your inbox!"); - toast.success("Email sent - check your inbox!"); - } else { - setEmailButtonText("Error sending email - try again?"); - toast.error("Error sending email - try again?"); + if (isPasswordMode) { + if (!password) { + toast.error("Password is required"); + return; } - setClickedMethod(undefined); - }); + + setClickedMethod("password"); + signIn("credentials", { + email: emailValidation.data, + password, + redirect: false, + ...(next && next.length > 0 ? { callbackUrl: next } : {}), + }).then((res) => { + if (res?.ok && !res?.error) { + setLastUsed("password"); + // Redirect will happen automatically + } else { + toast.error(res?.error || "Invalid credentials"); + } + setClickedMethod(undefined); + }); + } else { + setClickedMethod("email"); + signIn("email", { + email: emailValidation.data, + redirect: false, + ...(next && next.length > 0 ? { callbackUrl: next } : {}), + }).then((res) => { + if (res?.ok && !res?.error) { + setEmail(""); + setLastUsed("credentials"); + setEmailButtonText("Email sent - check your inbox!"); + toast.success("Email sent - check your inbox!"); + } else { + setEmailButtonText("Error sending email - try again?"); + toast.error("Error sending email - try again?"); + } + setClickedMethod(undefined); + }); + } }} >