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 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<AuthModeInfo | null>(null); // null = still loading
|
||||
|
||||
const [pendingUser, setPendingUser] = useState(null);
|
||||
const [pendingUser, setPendingUser] = useState<User | null>(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<string | null>(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 && (
|
||||
<img
|
||||
|
|
@ -257,7 +267,7 @@ export default function LoginPage() {
|
|||
alt=""
|
||||
aria-hidden="true"
|
||||
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}
|
||||
|
|
@ -301,7 +311,7 @@ export default function LoginPage() {
|
|||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-destructive bg-destructive/10
|
||||
<div className="text-sm text-destructive bg-destructive/10
|
||||
border border-destructive/20 rounded-md px-3 py-2">
|
||||
{error}
|
||||
</div>
|
||||
Loading…
Reference in New Issue