refactor(ts): LoginPage → TypeScript (auth-mode/TOTP/change-pw/privacy flows)
This commit is contained in:
parent
42e67dfdeb
commit
14f1fedfd0
|
|
@ -1,9 +1,9 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '@/api';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
@ -14,6 +14,14 @@ import {
|
||||||
|
|
||||||
const BUILD_LINK_URL = 'https://dream.scheller.ltd/null/BillTracker';
|
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() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setUser, refresh } = useAuth();
|
const { setUser, refresh } = useAuth();
|
||||||
|
|
@ -22,41 +30,43 @@ export default function LoginPage() {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [authMode, setAuthMode] = useState(null); // null = still loading
|
const [authMode, setAuthMode] = useState<AuthModeInfo | null>(null); // null = still loading
|
||||||
|
|
||||||
const [pendingUser, setPendingUser] = useState(null);
|
const [pendingUser, setPendingUser] = useState<User | null>(null);
|
||||||
const [showChangePw, setShowChangePw] = useState(false);
|
const [showChangePw, setShowChangePw] = useState(false);
|
||||||
const [showPrivacy, setShowPrivacy] = useState(false);
|
const [showPrivacy, setShowPrivacy] = useState(false);
|
||||||
|
|
||||||
// TOTP challenge state
|
// TOTP challenge state
|
||||||
const [totpChallenge, setTotpChallenge] = useState(null); // challenge_token string
|
const [totpChallenge, setTotpChallenge] = useState<string | null>(null); // challenge_token string
|
||||||
const [totpCode, setTotpCode] = useState('');
|
const [totpCode, setTotpCode] = useState('');
|
||||||
const [totpError, setTotpError] = useState('');
|
const [totpError, setTotpError] = useState('');
|
||||||
const [totpLoading, setTotpLoading] = useState(false);
|
const [totpLoading, setTotpLoading] = useState(false);
|
||||||
const [useRecovery, setUseRecovery] = useState(false);;
|
const [useRecovery, setUseRecovery] = useState(false);
|
||||||
|
|
||||||
const [newPw, setNewPw] = useState('');
|
const [newPw, setNewPw] = useState('');
|
||||||
const [confirmPw, setConfirmPw] = useState('');
|
const [confirmPw, setConfirmPw] = useState('');
|
||||||
const [pwLoading, setPwLoading] = useState(false);
|
const [pwLoading, setPwLoading] = useState(false);
|
||||||
|
|
||||||
const destFor = (user) => (user?.is_default_admin ? '/admin' : '/');
|
const destFor = (user?: User | null) => (user?.is_default_admin ? '/admin' : '/');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.authMode()
|
api.authMode()
|
||||||
.then(d => {
|
.then(raw => {
|
||||||
|
const d = raw as AuthModeInfo;
|
||||||
setAuthMode(d);
|
setAuthMode(d);
|
||||||
if (d.auth_mode === 'single') navigate('/', { replace: true });
|
if (d.auth_mode === 'single') navigate('/', { replace: true });
|
||||||
})
|
})
|
||||||
.catch(err => console.error('[LoginPage] authMode check failed', err));
|
.catch(err => console.error('[LoginPage] authMode check failed', err));
|
||||||
|
|
||||||
api.me()
|
api.me()
|
||||||
.then(d => {
|
.then(raw => {
|
||||||
|
const d = raw as { user?: User };
|
||||||
if (d.user) navigate(destFor(d.user), { replace: true });
|
if (d.user) navigate(destFor(d.user), { replace: true });
|
||||||
})
|
})
|
||||||
.catch(err => console.error('[LoginPage] session check failed', err));
|
.catch(err => console.error('[LoginPage] session check failed', err));
|
||||||
}, []); // eslint-disable-line
|
}, []); // eslint-disable-line
|
||||||
|
|
||||||
const handlePostLogin = (user) => {
|
const handlePostLogin = (user: User) => {
|
||||||
setUser(user);
|
setUser(user);
|
||||||
if (user.must_change_password) {
|
if (user.must_change_password) {
|
||||||
setPendingUser(user);
|
setPendingUser(user);
|
||||||
|
|
@ -71,14 +81,14 @@ export default function LoginPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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) {
|
if (data.requires_totp) {
|
||||||
setTotpChallenge(data.challenge_token);
|
setTotpChallenge(data.challenge_token ?? null);
|
||||||
setTotpCode('');
|
setTotpCode('');
|
||||||
setTotpError('');
|
setTotpError('');
|
||||||
setUseRecovery(false);
|
setUseRecovery(false);
|
||||||
|
|
@ -86,24 +96,24 @@ export default function LoginPage() {
|
||||||
}
|
}
|
||||||
handlePostLogin(data.user);
|
handlePostLogin(data.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Login failed.');
|
setError(errMessage(err, 'Login failed.'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTotpSubmit = async (e) => {
|
const handleTotpSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setTotpError('');
|
setTotpError('');
|
||||||
setTotpLoading(true);
|
setTotpLoading(true);
|
||||||
try {
|
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;
|
if (useRecovery) payload.recovery_code = totpCode;
|
||||||
else payload.code = totpCode;
|
else payload.code = totpCode;
|
||||||
const data = await api.totpChallenge(payload);
|
const data = await api.totpChallenge(payload) as { user: User };
|
||||||
handlePostLogin(data.user);
|
handlePostLogin(data.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setTotpError(err.message || 'Invalid code.');
|
setTotpError(errMessage(err, 'Invalid code.'));
|
||||||
setTotpCode('');
|
setTotpCode('');
|
||||||
} finally {
|
} finally {
|
||||||
setTotpLoading(false);
|
setTotpLoading(false);
|
||||||
|
|
@ -115,7 +125,7 @@ export default function LoginPage() {
|
||||||
const providerName = authMode?.oidc_provider_name || 'authentik';
|
const providerName = authMode?.oidc_provider_name || 'authentik';
|
||||||
const isAuthentikProvider = providerName.toLowerCase().includes('authentik');
|
const isAuthentikProvider = providerName.toLowerCase().includes('authentik');
|
||||||
|
|
||||||
const handleChangePassword = async (e) => {
|
const handleChangePassword = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (newPw !== confirmPw) {
|
if (newPw !== confirmPw) {
|
||||||
|
|
@ -141,7 +151,7 @@ export default function LoginPage() {
|
||||||
navigate(destFor(pendingUser), { replace: true });
|
navigate(destFor(pendingUser), { replace: true });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'Failed to change password.');
|
toast.error(errMessage(err, 'Failed to change password.'));
|
||||||
} finally {
|
} finally {
|
||||||
setPwLoading(false);
|
setPwLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +160,7 @@ export default function LoginPage() {
|
||||||
const handleAcknowledgePrivacy = async () => {
|
const handleAcknowledgePrivacy = async () => {
|
||||||
try {
|
try {
|
||||||
await api.acknowledgePrivacy();
|
await api.acknowledgePrivacy();
|
||||||
} catch {}
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
setShowPrivacy(false);
|
setShowPrivacy(false);
|
||||||
|
|
@ -249,7 +259,7 @@ export default function LoginPage() {
|
||||||
type="button"
|
type="button"
|
||||||
variant={localEnabled ? 'outline' : 'default'}
|
variant={localEnabled ? 'outline' : 'default'}
|
||||||
className="w-full gap-2"
|
className="w-full gap-2"
|
||||||
onClick={() => { window.location.href = authMode.oidc_login_url; }}
|
onClick={() => { window.location.href = authMode?.oidc_login_url || ''; }}
|
||||||
>
|
>
|
||||||
{isAuthentikProvider && (
|
{isAuthentikProvider && (
|
||||||
<img
|
<img
|
||||||
|
|
@ -257,7 +267,7 @@ export default function LoginPage() {
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="h-5 w-5 object-contain shrink-0"
|
className="h-5 w-5 object-contain shrink-0"
|
||||||
onError={e => { e.target.style.display = 'none'; }}
|
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
Continue with {providerName}
|
Continue with {providerName}
|
||||||
Loading…
Reference in New Issue