import { useEffect, useState } from 'react' import { Link, useNavigate, useLocation } from 'react-router-dom' import MoonDot from '../../components/MoonDot.jsx' import ProviderIcon from '../../components/ProviderIcon.jsx' import { useAuth } from '../../contexts/AuthContext.jsx' import { api } from '../../api/client.js' // Friendly copy for the ?sso_error codes the SSO callback can redirect back with. const SSO_ERRORS = { not_linked: 'That account is not linked to an admin user. Sign in with your password, then link it under Account.', disabled: 'This account is not active. Contact an administrator.', denied: 'Sign-in was cancelled.', unavailable: 'That sign-in method is not available right now.', bad_state: 'Your sign-in session expired. Please try again.', error: 'Could not complete sign-in. Please try again.', } const BG = "linear-gradient(180deg,rgba(11,15,20,0.72),rgba(11,15,20,0.9)),url('/assets/img/uomysticmoon-main-hero.png')" // Hidden anti-bot field. Off-screen via CSS (NOT display:none/hidden, which bots // skip) so real users never fill it but naive scripted bots do. Name must match // the server's HONEYPOT_FIELD ('company'). const honeypotStyle = { position: 'absolute', left: '-9999px', top: 'auto', width: '1px', height: '1px', opacity: 0, pointerEvents: 'none', } export default function AdminLogin() { const { user, login, loginTotp, ssoLoginTotp } = useAuth() const navigate = useNavigate() const location = useLocation() const dest = location.state?.from?.pathname || '/admin' const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [company, setCompany] = useState('') // honeypot — must stay empty const [error, setError] = useState('') const [busy, setBusy] = useState(false) // Two-factor step state. `ssoTotp` marks the SSO variant: the challenge lives in // an httpOnly cookie (not React state), so the code posts to a different endpoint. const [stage, setStage] = useState('creds') // 'creds' | 'totp' const [challenge, setChallenge] = useState('') const [code, setCode] = useState('') const [ssoTotp, setSsoTotp] = useState(false) // SSO providers to offer (empty if none configured) + any error the callback // bounced us back with (?sso_error=...). const [providers, setProviders] = useState([]) const ssoError = SSO_ERRORS[new URLSearchParams(location.search).get('sso_error')] || '' // Already signed in → go straight to the panel. useEffect(() => { if (user) navigate(dest, { replace: true }) }, [user, dest, navigate]) // The SSO callback bounces 2FA accounts back here with ?sso_totp=1 after the IdP // step: it has staged an httpOnly TOTP challenge and needs the authenticator code // before it will issue a session. Jump straight to the code step. useEffect(() => { if (new URLSearchParams(location.search).get('sso_totp')) { setStage('totp') setSsoTotp(true) } }, [location.search]) // Load enabled SSO providers for the buttons. Failure is non-fatal — the page // still works with password login and simply shows no provider buttons. useEffect(() => { let active = true api .authProviders() .then((list) => active && setProviders(Array.isArray(list) ? list : [])) .catch(() => active && setProviders([])) return () => { active = false } }, []) // Full-page redirect into the provider's OAuth flow, preserving the intended // destination so the callback can return the user there. function startSso(provider) { const q = dest && dest !== '/admin' ? `?returnTo=${encodeURIComponent(dest)}` : '' window.location.assign(provider.loginUrl + q) } async function onSubmit(e) { e.preventDefault() setError('') setBusy(true) try { const data = await login(username, password, { company }) if (data.totpRequired) { setChallenge(data.challenge) setStage('totp') setBusy(false) return } navigate(dest, { replace: true }) } catch (err) { setError(err.status === 401 ? 'Incorrect username or password.' : 'Could not sign in right now.') setBusy(false) } } async function onSubmitTotp(e) { e.preventDefault() setError('') setBusy(true) try { if (ssoTotp) { const { returnTo } = await ssoLoginTotp(code) navigate(returnTo || '/admin', { replace: true }) } else { await loginTotp(challenge, code) navigate(dest, { replace: true }) } } catch (err) { const expired = err.status === 401 && /expired/i.test(err.message) setError( expired ? 'Your verification session expired. Please sign in again.' : 'Invalid verification code.', ) setBusy(false) if (expired) { setStage('creds') setSsoTotp(false) } } } return (

UOMysticmoon

Admin Panel

{stage === 'creds' ? ( <> {/* Honeypot: hidden from humans, left empty; bots that fill it are rejected. */} ) : ( )} {(error || (stage === 'creds' && ssoError)) && (

{error || ssoError}

)} {/* SSO providers — only on the credentials step, only if any are enabled. */} {stage === 'creds' && providers.length > 0 && (
or
{providers.map((p) => ( ))}
)}

Protected area — not indexed. Sessions expire after 1 day.

← Back to site

) }