@@ -11,13 +11,11 @@ import { useNotify } from '@/lib/hooks/useNotify';
1111import dynamic from 'next/dynamic' ;
1212
1313import { loginSchema , LoginFormValues } from '@/lib/utils/validation' ;
14- import { normalizeEmail } from '@/lib/utils/sanitize' ;
1514import { useAuthStore } from '@/lib/store/authStore' ;
1615import { useRateLimitStore } from '@/lib/store/rateLimitStore' ;
1716import { Button } from '@/components/ui/button' ;
18- import { AuthInput } from '@/components/auth/AuthInput' ;
19- import { AuthLabel } from '@/components/auth/AuthLabel' ;
20- import { AuthButton } from '@/components/auth/AuthButton' ;
17+ import { Input } from '@/components/ui/input' ;
18+ import { Label } from '@/components/ui/label' ;
2119
2220import { WalletModalFallback } from '@/components/wallet/WalletModalFallback' ;
2321
@@ -53,47 +51,45 @@ export default function LoginPage() {
5351
5452 const onSubmit = useCallback ( async ( data : LoginFormValues ) => {
5553 setIsLoading ( true ) ;
56- const sanitizedData = { ...data , email : normalizeEmail ( data . email ) } ;
57-
5854 try {
5955 const apiBase = process . env . NEXT_PUBLIC_API_URL || 'https://bettapay-backend.onrender.com' ;
6056
61- // Authenticate against the real backend — no mock tokens
62- const loginRes = await fetch ( `${ apiBase } /api/auth/login ` , {
57+ // POST /api/auth/token expects { merchantId, secret }
58+ const loginRes = await fetch ( `${ apiBase } /api/auth/token ` , {
6359 method : 'POST' ,
6460 headers : { 'Content-Type' : 'application/json' } ,
6561 body : JSON . stringify ( {
66- email : sanitizedData . email ,
67- password : sanitizedData . password ,
62+ merchantId : data . merchantId ,
63+ secret : data . secret ,
6864 } ) ,
6965 } ) ;
7066
7167 if ( ! loginRes . ok ) {
7268 const errBody = await loginRes . json ( ) . catch ( ( ) => ( { } ) ) ;
7369 const message =
74- errBody ?. message ||
7570 errBody ?. error ||
76- ( loginRes . status === 401 ? 'Invalid email or password.' : 'Login failed. Please try again.' ) ;
71+ errBody ?. message ||
72+ ( loginRes . status === 401 ? 'Invalid Merchant ID or secret key.' : 'Login failed. Please try again.' ) ;
7773 error ( message ) ;
7874 return ;
7975 }
8076
81- const loginData = await loginRes . json ( ) ;
82-
83- // Backend returns { token, user: { id, email, name, role, ... } }
84- const token : string = loginData . token ?? loginData . data ?. token ;
85- const backendUser = loginData . user ?? loginData . data ?. user ;
77+ const { token } = await loginRes . json ( ) ;
8678
87- if ( ! token || ! backendUser ) {
79+ if ( ! token ) {
8880 error ( 'Unexpected response from server. Please try again.' ) ;
8981 return ;
9082 }
9183
84+ // Decode the JWT payload to get merchantId and role (no signature verification needed client-side)
85+ const payloadBase64 = token . split ( '.' ) [ 1 ] ;
86+ const payload = JSON . parse ( atob ( payloadBase64 ) ) ;
87+
9288 const user = {
93- id : backendUser . id ,
94- email : backendUser . email ,
95- name : backendUser . name ,
96- role : backendUser . role as 'admin' | 'merchant' ,
89+ id : payload . merchantId ?? data . merchantId ,
90+ email : '' ,
91+ name : 'Merchant' ,
92+ role : ( payload . role ?? 'merchant' ) as 'admin' | 'merchant' ,
9793 } ;
9894
9995 // Set the HttpOnly auth cookie via the Next.js session route
@@ -105,14 +101,11 @@ export default function LoginPage() {
105101 body : JSON . stringify ( { token, role : user . role } ) ,
106102 } ) ;
107103 } catch ( sessionErr ) {
108- // Cookie set failure is non-fatal — in-memory auth will still work for this tab
109104 console . warn ( 'Auth session cookie API unavailable.' , sessionErr ) ;
110105 }
111106
112- // Store token in-memory for the current session
113107 login ( token , user as import ( '@/lib/types' ) . User ) ;
114108 success ( 'Login successful' ) ;
115-
116109 router . push ( user . role === 'admin' ? '/overview' : '/dashboard' ) ;
117110 } catch ( err ) {
118111 console . error ( err ) ;
@@ -122,18 +115,13 @@ export default function LoginPage() {
122115 }
123116 } , [ login , router , success , error ] ) ;
124117
125- // When WalletModal reports a connected address, perform the merchant login flow.
126- // NOTE: Full wallet-challenge authentication (sign a nonce, verify on-chain) is
127- // the intended production approach — this flow requires a backend /api/auth/wallet
128- // endpoint that issues a real JWT after verifying the signed challenge.
118+ // Wallet login: wallet address becomes the merchant identifier.
119+ // A proper wallet-challenge JWT flow (sign nonce, verify on backend) is
120+ // the intended production path once the backend has /api/auth/wallet.
129121 const onWalletConnected = useCallback ( async ( address : string ) => {
130122 setIsWalletLoading ( true ) ;
131123 try {
132124 const role = 'merchant' ;
133-
134- // Placeholder: derive a session identifier from the wallet address.
135- // Replace this with a proper wallet-challenge JWT flow when the
136- // backend /api/auth/wallet endpoint is implemented.
137125 const walletSessionToken = `wallet_${ address } ` ;
138126
139127 try {
@@ -144,7 +132,7 @@ export default function LoginPage() {
144132 body : JSON . stringify ( { token : walletSessionToken , role } ) ,
145133 } ) ;
146134 } catch ( sessionErr ) {
147- console . warn ( 'Auth session API unavailable; continuing without HttpOnly cookie .' , sessionErr ) ;
135+ console . warn ( 'Auth session API unavailable.' , sessionErr ) ;
148136 }
149137
150138 login ( walletSessionToken , {
@@ -163,7 +151,6 @@ export default function LoginPage() {
163151 }
164152 } , [ login , router , success , error ] ) ;
165153
166-
167154 return (
168155 < div className = "w-full animate-in fade-in slide-in-from-bottom-4 duration-500" >
169156 < Suspense fallback = { < WalletModalFallback open = { walletModalOpen } onOpenChange = { setWalletModalOpen } /> } >
@@ -184,52 +171,58 @@ export default function LoginPage() {
184171
185172 { /* Form */ }
186173 < form onSubmit = { handleSubmit ( onSubmit ) } className = "space-y-5" >
187- { /* Email */ }
174+ { /* Merchant ID */ }
188175 < div className = "space-y-1.5" >
189- < AuthLabel htmlFor = "email" >
190- Email Address
191- </ AuthLabel >
192- < AuthInput
193- id = "email"
194- type = "email"
195- placeholder = "name@company.com"
196- { ...register ( 'email' ) }
197- aria-invalid = { errors . email ? "true" : "false" }
198- aria-describedby = { errors . email ? "email-error" : undefined }
176+ < Label htmlFor = "merchantId" className = "text-xs font-semibold text-muted-foreground uppercase tracking-wider" >
177+ Merchant ID
178+ </ Label >
179+ < Input
180+ id = "merchantId"
181+ type = "text"
182+ placeholder = "Your Stellar merchant address"
183+ { ...register ( 'merchantId' ) }
184+ aria-invalid = { errors . merchantId ? "true" : "false" }
185+ aria-describedby = { errors . merchantId ? "merchantId-error" : undefined }
186+ className = "h-12 bg-card border border-border text-foreground placeholder:text-muted-foreground rounded-xl text-sm font-mono focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-ring transition-all"
199187 />
200- { errors . email && < p id = "email -error" className = "text-xs text-destructive mt-1" > { errors . email . message } </ p > }
188+ { errors . merchantId && < p id = "merchantId -error" className = "text-xs text-destructive mt-1" > { errors . merchantId . message } </ p > }
201189 </ div >
202190
203- { /* Password */ }
191+ { /* Secret Key */ }
204192 < div className = "space-y-1.5" >
205193 < div className = "flex items-center justify-between" >
206- < AuthLabel htmlFor = "password " >
207- Password
208- </ AuthLabel >
194+ < Label htmlFor = "secret" className = "text-xs font-semibold text-muted-foreground uppercase tracking-wider ">
195+ Secret Key
196+ </ Label >
209197 < Link href = "/auth/forgot-password" className = "text-xs text-muted-foreground hover:text-primary transition-colors" >
210- Forgot password ?
198+ Lost your secret ?
211199 </ Link >
212200 </ div >
213- < AuthInput
214- id = "password "
201+ < Input
202+ id = "secret "
215203 type = "password"
216- placeholder = "••••••••"
217- { ...register ( 'password' ) }
218- aria-invalid = { errors . password ? "true" : "false" }
219- aria-describedby = { errors . password ? "password-error" : undefined }
204+ placeholder = "Your merchant secret key"
205+ { ...register ( 'secret' ) }
206+ aria-invalid = { errors . secret ? "true" : "false" }
207+ aria-describedby = { errors . secret ? "secret-error" : undefined }
208+ className = "h-12 bg-card border border-border text-foreground placeholder:text-muted-foreground rounded-xl text-sm focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-ring transition-all"
220209 />
221- { errors . password && < p id = "password-error" className = "text-xs text-destructive mt-1" > { errors . password . message } </ p > }
210+ { errors . secret && < p id = "secret-error" className = "text-xs text-destructive mt-1" > { errors . secret . message } </ p > }
211+ < p className = "text-xs text-muted-foreground" >
212+ The secret key shown once when you created your account.
213+ </ p >
222214 </ div >
223215
224216 { /* Sign In CTA */ }
225217 < div className = "pt-1" >
226- < AuthButton
218+ < Button
227219 type = "submit"
228220 disabled = { isLoading || isWalletLoading || isRateLimited }
221+ className = "w-full h-12 bg-primary hover:bg-primary/90 text-white font-semibold text-sm rounded-xl border-0 transition-colors"
229222 >
230223 { isLoading ? < Loader2 className = "mr-2 h-4 w-4 animate-spin" /> : null }
231224 { isRateLimited ? `Try again in ${ secondsRemaining } s` : 'Sign In' }
232- </ AuthButton >
225+ </ Button >
233226 </ div >
234227 </ form >
235228
0 commit comments