import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { api } from '@/api'; import { useAuth } from '@/hooks/useAuth'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { APP_VERSION } from '@/lib/version'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog'; const AUTHENTIK_ICON_URL = 'https://gate.originalsinners.org/static/dist/assets/icons/icon.png'; const BUILD_LINK_URL = 'https://dream.scheller.ltd/null/BillTracker'; export default function LoginPage() { const navigate = useNavigate(); const { setUser, refresh } = useAuth(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [authMode, setAuthMode] = useState({ local_enabled: true, oidc_enabled: false }); const [pendingUser, setPendingUser] = useState(null); const [showChangePw, setShowChangePw] = useState(false); const [showPrivacy, setShowPrivacy] = useState(false); const [newPw, setNewPw] = useState(''); const [confirmPw, setConfirmPw] = useState(''); const [pwLoading, setPwLoading] = useState(false); const destFor = (role) => (role === 'admin' ? '/admin' : '/'); useEffect(() => { api.authMode() .then(d => { setAuthMode(d); if (d.auth_mode === 'single') navigate('/', { replace: true }); }) .catch(() => {}); api.me() .then(d => { if (d.user) navigate(destFor(d.user.role), { replace: true }); }) .catch(() => {}); }, []); // eslint-disable-line const handlePostLogin = (user) => { setUser(user); if (user.must_change_password) { setPendingUser(user); setShowChangePw(true); } else if (user.first_login) { setPendingUser(user); setShowPrivacy(true); } else { navigate(destFor(user.role), { replace: true }); } }; const handleLogin = async (e) => { e.preventDefault(); setError(''); setLoading(true); try { const data = await api.login({ username, password }); handlePostLogin(data.user); } catch (err) { setError(err.message || 'Login failed.'); } finally { setLoading(false); } }; const localEnabled = authMode.local_enabled !== false; const oidcEnabled = !!authMode.oidc_enabled && !!authMode.oidc_login_url; const providerName = authMode.oidc_provider_name || 'authentik'; const handleChangePassword = async (e) => { e.preventDefault(); if (newPw !== confirmPw) { toast.error('Passwords do not match.'); return; } if (newPw.length < 6) { toast.error('Password must be at least 6 characters.'); return; } setPwLoading(true); try { await api.changePassword({ new_password: newPw }); refresh(); toast.success('Password updated.'); setShowChangePw(false); if (pendingUser?.first_login) { setShowPrivacy(true); } else { navigate(destFor(pendingUser.role), { replace: true }); } } catch (err) { toast.error(err.message || 'Failed to change password.'); } finally { setPwLoading(false); } }; const handleAcknowledgePrivacy = async () => { try { await api.acknowledgePrivacy(); } catch {} refresh(); setShowPrivacy(false); navigate(destFor(pendingUser?.role), { replace: true }); }; return (
{/* Logo / Brand */}
BillTracker
{/* Card */}

Sign in

{localEnabled ? 'Enter your credentials to continue.' : `Continue with ${providerName}.`}

{oidcEnabled && ( )} {localEnabled && oidcEnabled && (
or
)} {localEnabled && (
setUsername(e.target.value)} disabled={loading} required />
setPassword(e.target.value)} disabled={loading} required />
{error && (
{error}
)}
)}

Build v{APP_VERSION}

{/* Change Password Dialog */} e.preventDefault()}> Change your password You must set a new password before continuing.
setNewPw(e.target.value)} required />
setConfirmPw(e.target.value)} required />
{/* Privacy Dialog */} e.preventDefault()}> Privacy notice Please read before continuing.

Your financial data is stored privately and is accessible only to you.

What your administrator can do:

  • Reset your password if locked out
  • Cannot view your financial data
); }