import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { loadFrappeTranslations } from '../i18n'; import { bootstrapFrappeUserFromSession } from '../utils/bootstrapFrappeUserFromSession'; interface LoginFormData { email: string; password: string; } const AFTER_PASSWORD_RESET_KEY = 'asm_show_after_password_reset'; const TWO_FACTOR_TMP_ID_KEY = 'asm_login_tmp_id'; type LoginStep = 'credentials' | 'otp'; const Login: React.FC = () => { const [formData, setFormData] = useState({ email: '', password: '', }); const [loading, setLoading] = useState(false); const [checkingSession, setCheckingSession] = useState(true); const [error, setError] = useState(null); const [forgotOpen, setForgotOpen] = useState(false); const [forgotEmail, setForgotEmail] = useState(''); const [forgotLoading, setForgotLoading] = useState(false); const [forgotError, setForgotError] = useState(null); const [forgotMessage, setForgotMessage] = useState(false); const [pwdResetBusy, setPwdResetBusy] = useState(false); const [postResetBanner, setPostResetBanner] = useState(false); const [loginStep, setLoginStep] = useState('credentials'); const [tmpId, setTmpId] = useState(null); const [otpCode, setOtpCode] = useState(''); const [verification, setVerification] = useState<{ method: string; setup?: boolean; prompt?: string; } | null>(null); const navigate = useNavigate(); const location = useLocation(); const { t } = useTranslation(); const manualLoginHandledRef = useRef(false); const forgotAbortRef = useRef(null); const closeForgotModal = useCallback(() => { forgotAbortRef.current?.abort(); forgotAbortRef.current = null; setForgotOpen(false); setForgotLoading(false); setForgotError(null); setForgotMessage(false); }, []); const openForgotModal = useCallback(() => { setForgotOpen(true); setForgotEmail(formData.email); setForgotError(null); setForgotMessage(false); setForgotLoading(false); setError(null); }, [formData.email]); useEffect(() => { if (!forgotOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') closeForgotModal(); }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [forgotOpen, closeForgotModal]); useEffect(() => { let cancelled = false; (async () => { if ( typeof sessionStorage !== 'undefined' && sessionStorage.getItem(AFTER_PASSWORD_RESET_KEY) === '1' ) { sessionStorage.removeItem(AFTER_PASSWORD_RESET_KEY); if (!cancelled) setPostResetBanner(true); } const params = new URLSearchParams( typeof window !== 'undefined' ? window.location.search : '' ); if (params.get('manual_login') === '1' && !manualLoginHandledRef.current) { manualLoginHandledRef.current = true; if (!cancelled) setPwdResetBusy(true); localStorage.removeItem('user'); localStorage.removeItem('frappe_session_id'); const baseURL = import.meta.env.VITE_FRAPPE_BASE_URL || ''; let csrf = typeof window !== 'undefined' ? (window as { csrf_token?: string }).csrf_token : undefined; if (!csrf) { try { const csrfRes = await fetch(`${baseURL}/api/method/frappe.sessions.get_csrf_token`, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' }, }); if (csrfRes.ok) { const csrfData = (await csrfRes.json()) as { message?: string }; if (typeof csrfData.message === 'string') csrf = csrfData.message; } } catch { /* ignore */ } } const headers: Record = { Accept: 'application/json' }; if (csrf) headers['X-Frappe-CSRF-Token'] = csrf; try { await fetch(`${baseURL}/api/method/logout`, { method: 'POST', credentials: 'include', headers, }); } catch { /* ignore */ } if (typeof sessionStorage !== 'undefined') { sessionStorage.setItem(AFTER_PASSWORD_RESET_KEY, '1'); } if (cancelled) return; setPwdResetBusy(false); navigate('/login', { replace: true }); return; } if (localStorage.getItem('user')) { if (!cancelled) { setCheckingSession(false); navigate('/dashboard', { replace: true }); } return; } const result = await bootstrapFrappeUserFromSession(); if (cancelled) return; if (result.ok) { try { await loadFrappeTranslations(); } catch (err) { console.warn('Could not load translations after session bootstrap:', err); } if (!cancelled) navigate('/dashboard', { replace: true }); } if (!cancelled) setCheckingSession(false); })(); return () => { cancelled = true; }; }, [navigate, location.pathname, location.search]); const baseUrl = import.meta.env.BASE_URL || '/'; const logoVersion = import.meta.env.DEV ? `?v=${Date.now()}` : `?v=1774269853`; // Auto-updated by build script const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value, })); setError(null); }; const completeLogin = useCallback( async (user: { full_name?: string; user_id?: string; sid?: string; email?: string }) => { const apiService = (await import('../services/apiService')).default; const userData = { ...user, email: user.email || formData.email, }; localStorage.setItem('user', JSON.stringify(userData)); if (user.sid) { apiService.setSessionId(user.sid); } sessionStorage.removeItem(TWO_FACTOR_TMP_ID_KEY); try { await loadFrappeTranslations(); } catch (err) { console.warn('Could not load translations after login:', err); } navigate('/dashboard'); }, [formData.email, navigate] ); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); try { const apiService = (await import('../services/apiService')).default; const result = await apiService.login(formData); if (result.status === 'two_factor_required') { sessionStorage.setItem(TWO_FACTOR_TMP_ID_KEY, result.tmp_id); setTmpId(result.tmp_id); setVerification(result.verification); setLoginStep('otp'); setOtpCode(''); return; } await completeLogin(result.user); } catch (err: unknown) { console.error('Login error:', err); const message = err instanceof Error ? err.message : t('login.loginFailed'); setError(message); } finally { setLoading(false); } }; const handleOtpSubmit = async (e: React.FormEvent) => { e.preventDefault(); const storedTmpId = tmpId || sessionStorage.getItem(TWO_FACTOR_TMP_ID_KEY); if (!storedTmpId) { setError(t('login.twoFactorSessionExpired')); setLoginStep('credentials'); return; } if (!otpCode.trim()) { setError(t('login.twoFactorCodeRequired')); return; } setLoading(true); setError(null); try { const apiService = (await import('../services/apiService')).default; const result = await apiService.verifyLoginOtp(storedTmpId, otpCode); if (result.status === 'logged_in') { await completeLogin(result.user); } } catch (err: unknown) { const message = err instanceof Error ? err.message : t('login.twoFactorInvalid'); setError(message); } finally { setLoading(false); } }; const backToCredentials = () => { sessionStorage.removeItem(TWO_FACTOR_TMP_ID_KEY); setLoginStep('credentials'); setTmpId(null); setVerification(null); setOtpCode(''); setError(null); }; const handleDemoLogin = async () => { const demoUser = { full_name: 'Demo User', email: 'demo@seeraarabia.com', user_image: '', roles: ['System Manager', 'Administrator'] }; localStorage.setItem('user', JSON.stringify(demoUser)); // Load translations from Frappe after demo login try { await loadFrappeTranslations(); } catch (err) { console.warn('Could not load translations after demo login:', err); } navigate('/dashboard'); }; const handleForgotPasswordSubmit = async (e: React.FormEvent) => { e.preventDefault(); const userVal = forgotEmail.trim(); if (!userVal) { setForgotError(t('login.forgotPasswordUserRequired')); return; } forgotAbortRef.current?.abort(); const controller = new AbortController(); forgotAbortRef.current = controller; setForgotLoading(true); setForgotError(null); setForgotMessage(false); const forgotApi = await import('../services/apiService'); try { await forgotApi.default.requestPasswordReset(userVal, controller.signal); setForgotMessage(true); } catch (err: unknown) { if (err instanceof Error && err.name === 'AbortError') { setForgotError(t('login.forgotPasswordTimeout')); } else if (err instanceof forgotApi.ApiError) { if (err.code === 'USER_NOT_FOUND') setForgotError(t('login.forgotPasswordNotFound')); else if (err.code === 'RESET_NOT_ALLOWED') setForgotError(t('login.forgotPasswordCannotReset')); else if (err.code === 'FORBIDDEN') setForgotError(t('login.forgotPasswordFailed')); else if (err.code === 'EMPTY_EMAIL') setForgotError(t('login.forgotPasswordUserRequired')); else setForgotError(t('login.forgotPasswordFailed')); } else { setForgotError(t('login.forgotPasswordFailed')); } } finally { if (forgotAbortRef.current === controller) { forgotAbortRef.current = null; } setForgotLoading(false); } }; if (checkingSession || pwdResetBusy) { return (
{pwdResetBusy ? t('login.finishingSignOut') : t('common.loading')}
); } return (
Seera Arabia { const container = e.currentTarget.parentElement; if (container) { container.classList.add('bg-gradient-to-br', 'from-indigo-600', 'to-purple-600'); } e.currentTarget.style.display = 'none'; const nextSibling = e.currentTarget.nextElementSibling; if (nextSibling) { nextSibling.classList.remove('hidden'); } }} />

{t('login.title')}

{t('login.subtitle')}

{loginStep === 'otp' ? t('login.twoFactorTitle') : t('login.signIn')}

{postResetBanner && loginStep === 'credentials' && (

{t('login.afterPasswordResetSignIn')}

)} {loginStep === 'otp' ? (
{verification?.method === 'Email' && verification.prompt ? (

{verification.prompt}

) : verification?.method === 'OTP App' && verification.setup ? (

{t('login.twoFactorOtpAppEnter')}

) : verification?.method === 'OTP App' && !verification.setup ? (

{t('login.twoFactorOtpAppSetupIncomplete')}

) : (

{t('login.twoFactorOtpAppEnter')}

)} {verification?.method === 'Email' && (

{t('login.twoFactorEmailQrHint')}

)}
{ setOtpCode(ev.target.value.replace(/\D/g, '').slice(0, 6)); setError(null); }} />
{error && (
{error}
)}
) : (
{error && (
{error}
)}
)} {loginStep === 'credentials' && (
)}
{forgotOpen && (
ev.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="forgot-password-title" >

{t('login.forgotPasswordTitle')}

{t('login.forgotPasswordHint')}

{ setForgotEmail(ev.target.value); setForgotError(null); setForgotMessage(false); }} className="relative block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-500 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder-gray-400" placeholder={t('login.forgotPasswordUserPlaceholder')} /> {forgotError && (
{forgotError}
)} {forgotMessage && (
{t('login.forgotPasswordSentSuccess')}
)}
)}
); }; export default Login; // import React, { useState } from 'react'; // import { useNavigate } from 'react-router-dom'; // import { useAuth } from '../hooks/useApi'; // interface LoginFormData { // email: string; // password: string; // } // const Login: React.FC = () => { // const [formData, setFormData] = useState({ // email: '', // password: '', // }); // const [loading, setLoading] = useState(false); // const [error, setError] = useState(null); // const navigate = useNavigate(); // const { login } = useAuth(); // const handleChange = (e: React.ChangeEvent) => { // const { name, value } = e.target; // setFormData(prev => ({ // ...prev, // [name]: value, // })); // setError(null); // }; // const handleSubmit = async (e: React.FormEvent) => { // e.preventDefault(); // setLoading(true); // setError(null); // try { // const response = await login(formData); // if (response && response.message) { // // Store user info in localStorage with email field // const userData = { // ...response.message, // email: formData.email // Ensure email is stored // }; // localStorage.setItem('user', JSON.stringify(userData)); // navigate('/dashboard'); // } else { // setError('Login failed. Please check your credentials.'); // } // } catch (err: any) { // setError(err.message || 'Login failed. Please try again.'); // } finally { // setLoading(false); // } // }; // const handleDemoLogin = () => { // // Create dummy user data for demo purposes // const demoUser = { // full_name: 'Demo User', // email: 'demo@seeraarabia.com', // user_image: '', // roles: ['System Manager', 'Administrator'] // }; // // Store demo user in localStorage // localStorage.setItem('user', JSON.stringify(demoUser)); // navigate('/dashboard'); // }; // return ( //
//
//
//
//
// {/* Seera Arabia Logo */} // Seera Arabia { // // Fallback to gradient background with SVG if image not found // const container = e.currentTarget.parentElement; // if (container) { // container.classList.add('bg-gradient-to-br', 'from-indigo-600', 'to-purple-600'); // } // e.currentTarget.style.display = 'none'; // e.currentTarget.nextElementSibling?.classList.remove('hidden'); // }} // /> // // // // // //
//
//

// Seera Arabia //

//

// Asset Management System //

//

// Sign in to continue //

//
//
//
//
// // //
//
// // //
//
// {error && ( //
//
{error}
//
// )} //
// //
//
//
//
//
// or //
//
// //
// //
//
// ); // }; // export default Login;