11"use client" ;
22
33import { useLocale } from "next-intl" ;
4- import { Link } from "@/i18n/routing" ;
5- import { useState } from "react" ;
64import { useSearchParams } from "next/navigation" ;
7- import { Button } from "@/components/ui/button" ;
8- import { OAuthButtons } from "@/components/auth/OAuthButtons" ;
9-
10- function isSafeRedirectUrl ( url : string ) : boolean {
11- if ( ! url . startsWith ( "/" ) ) return false ;
12- if ( url . startsWith ( "//" ) ) return false ;
13- if ( url . includes ( "://" ) ) return false ;
14- return true ;
15- }
5+ import { LoginForm } from "@/components/auth/LoginForm" ;
6+ import { getSafeRedirect } from "@/lib/auth/safe-redirect" ;
167
178export default function LoginPage ( ) {
18- const searchParams = useSearchParams ( ) ;
199 const locale = useLocale ( ) ;
10+ const searchParams = useSearchParams ( ) ;
2011
21- const returnToParam = searchParams . get ( "returnTo" ) ;
22- const returnTo = returnToParam ?? "" ;
23-
24- const [ loading , setLoading ] = useState ( false ) ;
25- const [ errorMessage , setErrorMessage ] =
26- useState < string | null > ( null ) ;
27- const [ errorCode , setErrorCode ] =
28- useState < string | null > ( null ) ;
29- const [ email , setEmail ] = useState ( "" ) ;
30- const [ verificationSent , setVerificationSent ] =
31- useState ( false ) ;
32- const [ showPassword , setShowPassword ] =
33- useState ( false ) ;
34-
35- async function onSubmit (
36- e : React . FormEvent < HTMLFormElement >
37- ) {
38- e . preventDefault ( ) ;
39- setLoading ( true ) ;
40- setErrorMessage ( null ) ;
41- setErrorCode ( null ) ;
42- setVerificationSent ( false ) ;
43-
44- const formData = new FormData ( e . currentTarget ) ;
45- const emailValue = String ( formData . get ( "email" ) || "" ) ;
46- setEmail ( emailValue ) ;
47-
48- try {
49- const res = await fetch ( "/api/auth/login" , {
50- method : "POST" ,
51- headers : { "Content-Type" : "application/json" } ,
52- body : JSON . stringify ( {
53- email : emailValue ,
54- password : formData . get ( "password" ) ,
55- } ) ,
56- } ) ;
57-
58- const data = await res . json ( ) . catch ( ( ) => null ) ;
59-
60- if ( ! res . ok ) {
61- setErrorCode ( data ?. code ?? null ) ;
62-
63- if ( data ?. code === "EMAIL_NOT_VERIFIED" ) {
64- setErrorMessage (
65- "Your email address is not verified. Please check your inbox."
66- ) ;
67- } else {
68- setErrorMessage ( "Invalid email or password" ) ;
69- }
70- return ;
71- }
72-
73- const redirectTarget =
74- returnTo && isSafeRedirectUrl ( returnTo )
75- ? returnTo
76- : `/${ locale } /dashboard` ;
77-
78- window . location . href = redirectTarget ;
79- } catch ( err ) {
80- console . error ( "Login request failed:" , err ) ;
81- setErrorMessage (
82- "Network error. Please check your connection and try again."
83- ) ;
84- setErrorCode ( null ) ;
85- } finally {
86- setLoading ( false ) ;
87- }
88- }
89-
90- async function resendVerification ( ) {
91- if ( ! email ) return ;
92-
93- await fetch ( "/api/auth/resend-verification" , {
94- method : "POST" ,
95- headers : { "Content-Type" : "application/json" } ,
96- body : JSON . stringify ( { email } ) ,
97- } ) ;
98-
99- setVerificationSent ( true ) ;
100- setErrorCode ( null ) ;
101- setErrorMessage ( null ) ;
102- }
12+ const returnTo = getSafeRedirect (
13+ searchParams . get ( "returnTo" )
14+ ) ;
10315
10416 return (
105- < div className = "mx-auto max-w-sm py-12" >
106- < h1 className = "mb-6 text-2xl font-semibold" >
107- Log in
108- </ h1 >
109-
110- < OAuthButtons />
111-
112- < div className = "my-4 flex items-center gap-3" >
113- < div className = "h-px flex-1 bg-gray-200" />
114- < span className = "text-xs text-gray-500" >
115- or
116- </ span >
117- < div className = "h-px flex-1 bg-gray-200" />
118- </ div >
119-
120- < form onSubmit = { onSubmit } className = "space-y-4" >
121- < input
122- name = "email"
123- type = "email"
124- placeholder = "Email"
125- required
126- className = "w-full rounded border px-3 py-2"
127- onChange = { e => setEmail ( e . target . value ) }
128- />
129-
130- < div className = "relative" >
131- < input
132- name = "password"
133- type = { showPassword ? "text" : "password" }
134- placeholder = "Password"
135- required
136- className = "w-full rounded border px-3 py-2 pr-10"
137- />
138-
139- < button
140- type = "button"
141- aria-label = {
142- showPassword
143- ? "Hide password"
144- : "Show password"
145- }
146- onClick = { ( ) =>
147- setShowPassword ( v => ! v )
148- }
149- className = "absolute inset-y-0 right-2 flex items-center text-sm text-gray-500"
150- >
151- { showPassword ? "Hide" : "Show" }
152- </ button >
153- </ div >
154-
155- < div className = "text-right" >
156- < Link
157- href = {
158- returnTo
159- ? `/forgot-password?returnTo=${ encodeURIComponent (
160- returnTo
161- ) } `
162- : "/forgot-password"
163- }
164- className = "text-sm underline text-gray-600"
165- >
166- Forgot password?
167- </ Link >
168- </ div >
169-
170- { errorMessage && ! verificationSent && (
171- < div className = "rounded-md border border-yellow-400 bg-yellow-50 p-3 text-sm text-yellow-800" >
172- < p > { errorMessage } </ p >
173-
174- { errorCode === "EMAIL_NOT_VERIFIED" && (
175- < button
176- type = "button"
177- onClick = { resendVerification }
178- className = "mt-2 underline"
179- >
180- Resend verification email
181- </ button >
182- ) }
183- </ div >
184- ) }
185-
186- { verificationSent && (
187- < div className = "rounded-md border border-green-400 bg-green-50 p-3 text-sm text-green-800" >
188- Verification successfully sent to{ " " }
189- < strong > { email } </ strong >
190- </ div >
191- ) }
192-
193- < Button
194- type = "submit"
195- disabled = { loading }
196- className = "w-full"
197- >
198- { loading ? "Logging in..." : "Log in" }
199- </ Button >
200- </ form >
201-
202- < p className = "mt-4 text-sm text-gray-600" >
203- Don’t have an account?{ " " }
204- < Link
205- href = {
206- returnTo
207- ? `/signup?returnTo=${ encodeURIComponent (
208- returnTo
209- ) } `
210- : "/signup"
211- }
212- className = "underline"
213- >
214- Sign up
215- </ Link >
216- </ p >
217- </ div >
17+ < LoginForm
18+ locale = { locale }
19+ returnTo = { returnTo }
20+ />
21821 ) ;
21922}
0 commit comments