diff --git a/app/(auth)/forgot-password/page.tsx b/app/(auth)/forgot-password/page.tsx
new file mode 100644
index 0000000..71eeeed
--- /dev/null
+++ b/app/(auth)/forgot-password/page.tsx
@@ -0,0 +1,5 @@
+import ForgotPasswordForm from "@/components/auth/forgot-password-form"
+
+export default function ForgotPasswordPage() {
+ return
+}
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..ab21ea4
--- /dev/null
+++ b/app/(auth)/login/page.tsx
@@ -0,0 +1,5 @@
+import LoginForm from "@/components/auth/login-form"
+
+export default function LoginPage() {
+ return
+}
diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx
new file mode 100644
index 0000000..98ccaed
--- /dev/null
+++ b/app/(auth)/reset-password/page.tsx
@@ -0,0 +1,21 @@
+import { Suspense } from "react"
+import ResetPasswordForm from "@/components/auth/reset-password-form"
+import { redirect } from "next/navigation"
+
+interface PageProps {
+ searchParams: Promise<{ email?: string }>
+}
+
+export default async function ResetPasswordPage({ searchParams }: PageProps) {
+ const { email } = await searchParams
+
+ if (!email) {
+ redirect("/forgot-password")
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/components/auth/forgot-password-form.tsx b/components/auth/forgot-password-form.tsx
new file mode 100644
index 0000000..646d8d4
--- /dev/null
+++ b/components/auth/forgot-password-form.tsx
@@ -0,0 +1,96 @@
+"use client"
+
+import { useState } from "react"
+import { Lock, Loader2, ArrowLeft } from "lucide-react"
+import { authClient } from "@/lib/auth-client"
+import {
+ forgotPasswordSchema,
+ type ForgotPasswordInput,
+} from "@/lib/auth/validation"
+import Link from "next/link"
+import { useRouter } from "next/navigation"
+
+export default function ForgotPasswordForm() {
+ const router = useRouter()
+ const [email, setEmail] = useState("")
+ const [error, setError] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError("")
+
+ const result = forgotPasswordSchema.safeParse({ email })
+ if (!result.success) {
+ setError(result.error.issues[0].message)
+ return
+ }
+
+ setIsLoading(true)
+ const { error } = await authClient.emailOtp.requestPasswordReset({
+ email,
+ })
+ setIsLoading(false)
+
+ if (error) {
+ setError(
+ error.message || "Something went wrong. Please try again.",
+ )
+ return
+ }
+
+ // Redirect to reset password page with email
+ router.push(`/reset-password?email=${encodeURIComponent(email)}`)
+ }
+
+ return (
+
+ )
+}
diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx
new file mode 100644
index 0000000..c9d06cb
--- /dev/null
+++ b/components/auth/login-form.tsx
@@ -0,0 +1,234 @@
+"use client"
+
+import { useState, useEffect, useCallback } from "react"
+import { Loader2 } from "lucide-react"
+import { authClient } from "@/lib/auth-client"
+import {
+ loginEmailSchema,
+ loginSchema,
+ type LoginInput,
+} from "@/lib/auth/validation"
+import SocialButtons from "./social-buttons"
+import Link from "next/link"
+import { useRouter } from "next/navigation"
+
+export default function LoginForm() {
+ const router = useRouter()
+ const [phase, setPhase] = useState<"email" | "password">("email")
+ const [email, setEmail] = useState("")
+ const [password, setPassword] = useState("")
+ const [error, setError] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+
+ // Preload passkeys for conditional UI
+ useEffect(() => {
+ if (phase === "password" && "PublicKeyCredential" in window) {
+ authClient.signIn.passkey({ autoFill: true })
+ }
+ }, [phase])
+
+ const handleEmailSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError("")
+
+ const result = loginEmailSchema.safeParse({ email })
+ if (!result.success) {
+ setError(result.error.issues[0].message)
+ return
+ }
+
+ // Check if email exists by attempting sign-in with empty password
+ setIsLoading(true)
+ const { error } = await authClient.signIn.email(
+ { email, password: "" },
+ {
+ onError: () => {
+ // Silently handle - we check the error type below
+ },
+ },
+ )
+ setIsLoading(false)
+
+ if (error) {
+ // If error is about invalid credentials, email exists but password is wrong
+ // If error is about user not found, email doesn't exist
+ if (
+ error.message?.toLowerCase().includes("not found") ||
+ error.message?.toLowerCase().includes("invalid email") ||
+ error.status === 404
+ ) {
+ setError(
+ "No account found with this email.",
+ )
+ return
+ }
+ // Email exists, move to password phase
+ setPhase("password")
+ return
+ }
+
+ // Shouldn't reach here with empty password, but handle it
+ setPhase("password")
+ }
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError("")
+
+ const result = loginSchema.safeParse({ email, password })
+ if (!result.success) {
+ setError(result.error.issues[0].message)
+ return
+ }
+
+ setIsLoading(true)
+ const { error } = await authClient.signIn.email({ email, password })
+ setIsLoading(false)
+
+ if (error) {
+ setError(error.message || "Invalid credentials. Please try again.")
+ return
+ }
+
+ router.push("/")
+ }
+
+ const handleChangeEmail = () => {
+ setPhase("email")
+ setPassword("")
+ setError("")
+ }
+
+ return (
+
+
+
+ Welcome back
+
+
+ {phase === "email"
+ ? "Sign in to DeckyVault"
+ : `Signing in as `}
+ {phase === "password" && (
+ <>
+ {email}
+ {" "}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ or continue with email
+
+
+
+
+ {error && (
+
+ {error}
+ {error.includes("No account found") && (
+ <>
+ {" "}
+
+ Create one →
+
+ >
+ )}
+
+ )}
+
+ {phase === "email" ? (
+
+ ) : (
+
+ )}
+
+
+ Don't have an account?{" "}
+
+ Create one
+
+
+
+ )
+}
diff --git a/components/auth/reset-password-form.tsx b/components/auth/reset-password-form.tsx
new file mode 100644
index 0000000..6fa25ac
--- /dev/null
+++ b/components/auth/reset-password-form.tsx
@@ -0,0 +1,222 @@
+"use client"
+
+import { useState, useEffect, useCallback } from "react"
+import {
+ Mail,
+ CheckCircle2,
+ Loader2,
+ ArrowLeft,
+} from "lucide-react"
+import { authClient } from "@/lib/auth-client"
+import {
+ resetPasswordSchema,
+ type ResetPasswordInput,
+} from "@/lib/auth/validation"
+import OtpInput from "./otp-input"
+import PasswordStrengthMeter from "./password-strength"
+import Link from "next/link"
+
+interface ResetPasswordFormProps {
+ email: string
+}
+
+export default function ResetPasswordForm({
+ email,
+}: ResetPasswordFormProps) {
+ const [otp, setOtp] = useState("")
+ const [newPassword, setNewPassword] = useState("")
+ const [confirmPassword, setConfirmPassword] = useState("")
+ const [error, setError] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+ const [resendTimer, setResendTimer] = useState(300)
+ const [canResend, setCanResend] = useState(false)
+
+ // Countdown timer
+ useEffect(() => {
+ if (resendTimer <= 0) {
+ setCanResend(true)
+ return
+ }
+ const interval = setInterval(() => {
+ setResendTimer((prev) => prev - 1)
+ }, 1000)
+ return () => clearInterval(interval)
+ }, [resendTimer])
+
+ const formatTime = (seconds: number) => {
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins}:${secs.toString().padStart(2, "0")}`
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError("")
+
+ const result = resetPasswordSchema.safeParse({
+ otp,
+ newPassword,
+ confirmPassword,
+ })
+ if (!result.success) {
+ setError(result.error.issues[0].message)
+ return
+ }
+
+ setIsLoading(true)
+ const { error } = await authClient.resetPassword({
+ newPassword,
+ otp,
+ })
+ setIsLoading(false)
+
+ if (error) {
+ setError(
+ error.code === "TOO_MANY_ATTEMPTS"
+ ? "Too many attempts. Please request a new code."
+ : error.message || "Failed to reset password.",
+ )
+ return
+ }
+
+ setIsSuccess(true)
+ }
+
+ const handleResend = async () => {
+ setError("")
+ await authClient.emailOtp.requestPasswordReset({ email })
+ setResendTimer(300)
+ setCanResend(false)
+ }
+
+ if (isSuccess) {
+ return (
+
+
+
+ Password reset successful
+
+
+ Your password has been updated. You can now sign in with
+ your new password.
+
+
+ Sign in
+
+
+ )
+ }
+
+ return (
+
+ )
+}