From 14f1fedfd086df8869deb3ed78c0c67fec6503eb Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Jul 2026 22:23:31 -0500 Subject: [PATCH] =?UTF-8?q?refactor(ts):=20LoginPage=20=E2=86=92=20TypeScr?= =?UTF-8?q?ipt=20(auth-mode/TOTP/change-pw/privacy=20flows)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/pages/{LoginPage.jsx => LoginPage.tsx} | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) rename client/pages/{LoginPage.jsx => LoginPage.tsx} (87%) diff --git a/client/pages/LoginPage.jsx b/client/pages/LoginPage.tsx similarity index 87% rename from client/pages/LoginPage.jsx rename to client/pages/LoginPage.tsx index 3275e77..8fc180d 100644 --- a/client/pages/LoginPage.jsx +++ b/client/pages/LoginPage.tsx @@ -1,9 +1,9 @@ - -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { api } from '@/api'; -import { useAuth } from '@/hooks/useAuth'; +import { errMessage } from '@/lib/utils'; +import { useAuth, type User } from '@/hooks/useAuth'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -14,6 +14,14 @@ import { const BUILD_LINK_URL = 'https://dream.scheller.ltd/null/BillTracker'; +interface AuthModeInfo { + auth_mode?: string; + local_enabled?: boolean; + oidc_enabled?: boolean; + oidc_login_url?: string; + oidc_provider_name?: string; +} + export default function LoginPage() { const navigate = useNavigate(); const { setUser, refresh } = useAuth(); @@ -22,41 +30,43 @@ export default function LoginPage() { const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - const [authMode, setAuthMode] = useState(null); // null = still loading + const [authMode, setAuthMode] = useState(null); // null = still loading - const [pendingUser, setPendingUser] = useState(null); + const [pendingUser, setPendingUser] = useState(null); const [showChangePw, setShowChangePw] = useState(false); const [showPrivacy, setShowPrivacy] = useState(false); // TOTP challenge state - const [totpChallenge, setTotpChallenge] = useState(null); // challenge_token string + const [totpChallenge, setTotpChallenge] = useState(null); // challenge_token string const [totpCode, setTotpCode] = useState(''); const [totpError, setTotpError] = useState(''); const [totpLoading, setTotpLoading] = useState(false); - const [useRecovery, setUseRecovery] = useState(false);; + const [useRecovery, setUseRecovery] = useState(false); const [newPw, setNewPw] = useState(''); const [confirmPw, setConfirmPw] = useState(''); const [pwLoading, setPwLoading] = useState(false); - const destFor = (user) => (user?.is_default_admin ? '/admin' : '/'); + const destFor = (user?: User | null) => (user?.is_default_admin ? '/admin' : '/'); useEffect(() => { api.authMode() - .then(d => { + .then(raw => { + const d = raw as AuthModeInfo; setAuthMode(d); if (d.auth_mode === 'single') navigate('/', { replace: true }); }) .catch(err => console.error('[LoginPage] authMode check failed', err)); api.me() - .then(d => { + .then(raw => { + const d = raw as { user?: User }; if (d.user) navigate(destFor(d.user), { replace: true }); }) .catch(err => console.error('[LoginPage] session check failed', err)); }, []); // eslint-disable-line - const handlePostLogin = (user) => { + const handlePostLogin = (user: User) => { setUser(user); if (user.must_change_password) { setPendingUser(user); @@ -71,14 +81,14 @@ export default function LoginPage() { } }; - const handleLogin = async (e) => { + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { - const data = await api.login({ username, password }); + const data = await api.login({ username, password }) as { requires_totp?: boolean; challenge_token?: string; user: User }; if (data.requires_totp) { - setTotpChallenge(data.challenge_token); + setTotpChallenge(data.challenge_token ?? null); setTotpCode(''); setTotpError(''); setUseRecovery(false); @@ -86,24 +96,24 @@ export default function LoginPage() { } handlePostLogin(data.user); } catch (err) { - setError(err.message || 'Login failed.'); + setError(errMessage(err, 'Login failed.')); } finally { setLoading(false); } }; - const handleTotpSubmit = async (e) => { + const handleTotpSubmit = async (e: React.FormEvent) => { e.preventDefault(); setTotpError(''); setTotpLoading(true); try { - const payload = { challenge_token: totpChallenge }; + const payload: { challenge_token: string | null; recovery_code?: string; code?: string } = { challenge_token: totpChallenge }; if (useRecovery) payload.recovery_code = totpCode; else payload.code = totpCode; - const data = await api.totpChallenge(payload); + const data = await api.totpChallenge(payload) as { user: User }; handlePostLogin(data.user); } catch (err) { - setTotpError(err.message || 'Invalid code.'); + setTotpError(errMessage(err, 'Invalid code.')); setTotpCode(''); } finally { setTotpLoading(false); @@ -115,7 +125,7 @@ export default function LoginPage() { const providerName = authMode?.oidc_provider_name || 'authentik'; const isAuthentikProvider = providerName.toLowerCase().includes('authentik'); - const handleChangePassword = async (e) => { + const handleChangePassword = async (e: React.FormEvent) => { e.preventDefault(); if (newPw !== confirmPw) { @@ -141,7 +151,7 @@ export default function LoginPage() { navigate(destFor(pendingUser), { replace: true }); } } catch (err) { - toast.error(err.message || 'Failed to change password.'); + toast.error(errMessage(err, 'Failed to change password.')); } finally { setPwLoading(false); } @@ -150,7 +160,7 @@ export default function LoginPage() { const handleAcknowledgePrivacy = async () => { try { await api.acknowledgePrivacy(); - } catch {} + } catch { /* ignore */ } refresh(); setShowPrivacy(false); @@ -249,7 +259,7 @@ export default function LoginPage() { type="button" variant={localEnabled ? 'outline' : 'default'} className="w-full gap-2" - onClick={() => { window.location.href = authMode.oidc_login_url; }} + onClick={() => { window.location.href = authMode?.oidc_login_url || ''; }} > {isAuthentikProvider && ( { e.target.style.display = 'none'; }} + onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} /> )} Continue with {providerName} @@ -301,7 +311,7 @@ export default function LoginPage() { {error && ( -
{error}