import { useEffect, useState, type FormEvent } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import * as authApi from '../../api/auth'; import { ApiError } from '../../api/http'; import { DEFAULT_TENANT, oauthRedirectUri } from '../../config'; import { useAuth } from '../../context/AuthContext'; import * as permApi from '../../api/permission'; export function LoginPage() { const navigate = useNavigate(); const location = useLocation(); const { syncSession, refreshRoles } = useAuth(); const [tenant, setTenant] = useState( () => localStorage.getItem('tenant_slug') ?? DEFAULT_TENANT, ); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [mfaChallengeId, setMfaChallengeId] = useState(null); const [totpCode, setTotpCode] = useState(''); const [error, setError] = useState(''); const [info, setInfo] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { const state = location.state as { message?: string; mfa_challenge_id?: string; tenant?: string; } | null; if (state?.tenant) { setTenant(state.tenant); localStorage.setItem('tenant_slug', state.tenant); } if (state?.mfa_challenge_id) { setMfaChallengeId(state.mfa_challenge_id); setInfo(state.message ?? '請輸入驗證碼以完成登入'); navigate(location.pathname, { replace: true, state: null }); return; } if (state?.message) { setInfo(state.message); navigate(location.pathname, { replace: true, state: null }); } }, [location.pathname, location.state, navigate]); const startFederated = async (provider: authApi.FederatedProvider) => { setError(''); setLoading(true); try { localStorage.setItem('tenant_slug', tenant); const { oauth_url } = await authApi.loginSocialStart( tenant, provider, oauthRedirectUri('/auth/callback/login'), ); authApi.startOAuthRedirect(oauth_url); } catch (err) { setError(err instanceof ApiError ? err.message : '無法啟動登入'); setLoading(false); } }; const finishLogin = async () => { syncSession(); await refreshRoles(); const me = await permApi.getMyPermissions(); const admin = permApi.isAdminRole(me.roles ?? []); navigate(admin ? '/admin' : '/app'); }; const submit = async (e: FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { localStorage.setItem('tenant_slug', tenant); const result = await authApi.login(tenant, email, password); if (result.mfa_required || result.mfa_challenge_id) { if (!result.mfa_challenge_id) { throw new Error('缺少 MFA challenge'); } setMfaChallengeId(result.mfa_challenge_id); setTotpCode(''); setInfo(''); return; } await finishLogin(); } catch (err) { if (err instanceof ApiError && err.code === 28505000) { setError('帳號尚未完成 Email 驗證,請先完成註冊驗證。'); } else { setError(err instanceof ApiError ? err.message : '登入失敗'); } } finally { setLoading(false); } }; const submitMfa = async (e: FormEvent) => { e.preventDefault(); if (!mfaChallengeId) return; setError(''); setLoading(true); try { const code = totpCode.replace(/\s/g, ''); await authApi.loginMfaConfirm(tenant, mfaChallengeId, code); await finishLogin(); } catch (err) { setError(err instanceof ApiError ? err.message : '驗證碼錯誤'); } finally { setLoading(false); } }; const backToPassword = () => { setMfaChallengeId(null); setTotpCode(''); setError(''); }; if (mfaChallengeId) { return (

雙因素驗證

{email ? ( <> 帳號 {email} 已啟用 TOTP。 ) : ( <>此帳號已啟用 TOTP。 )} 請輸入驗證器 App 的 6 位數驗證碼,或一次性備援碼。

{error &&

{error}

}
); } return (

登入

{error &&

{error}

} {info &&

{info}

}

還沒有帳號? 註冊 {' · '} 尚未完成驗證? {' · '} 忘記密碼?

); }