import { Box, Button, Checkbox, CircularProgress, Container, FormControlLabel, Grid, Link, styled, TextField, Typography } from "@mui/material"; import {Alert} from '@mui/lab'; import React, {useCallback, useContext, useEffect, useRef, useState} from "react"; import ReplayIcon from '@mui/icons-material/Replay'; import LanguageSelection from "../elements/language-selection"; import {decodeText, encodeText, getParameter, removeParameter} from "shared/util"; import {LocaleContext} from "shared/locale"; const LoginContainer = styled(Container)((props) => ({ marginTop: props.theme.spacing(5), paddingBottom: props.theme.spacing(1), borderColor: props.theme.palette.primary.main, borderStyle: "solid", borderWidth: 1, borderRadius: 5, "& h1 > img": { marginRight: props.theme.spacing(2), width: "60px", height: "60px" } })); const ResponseAlert = styled(Alert)((props) => ({ marginBottom: props.theme.spacing(2), })); export default function LoginForm(props) { const api = props.api; // inputs let [username, setUsername] = useState(""); let [password, setPassword] = useState(""); let [rememberMe, setRememberMe] = useState(true); let [emailConfirmed, setEmailConfirmed] = useState(null); let [tfaCode, set2FACode] = useState(""); // 2fa // 0: not sent, 1: sent, 2: retry let [tfaToken, set2FAToken] = useState(api.user?.twoFactorToken || { authenticated: false, type: null, step: 0 }); let [error, setError] = useState(""); const abortController = new AbortController(); const abortSignal = abortController.signal; // state let [isLoggingIn, setLoggingIn] = useState(false); let [loaded, setLoaded] = useState(false); // ui let passwordRef = useRef(); const {translate: L, currentLocale, requestModules} = useContext(LocaleContext); const onUpdateLocale = useCallback(() => { requestModules(api, ["general", "account"], currentLocale).then(data => { setLoaded(true); if (!data.success) { alert(data.msg); } }); }, [currentLocale]); useEffect(() => { onUpdateLocale(); }, [currentLocale]); const onLogin = useCallback(() => { if (!isLoggingIn) { setError(""); setLoggingIn(true); removeParameter("success"); api.login(username, password, rememberMe).then((res) => { let twoFactorToken = res.twoFactorToken || { }; set2FAToken({ ...twoFactorToken, authenticated: false, step: 0, error: "" }); setLoggingIn(false); setPassword(""); if (!res.success) { setEmailConfirmed(res.emailConfirmed); setError(res.msg); } else if (!twoFactorToken.type) { props.onLogin(); } }); } }, [api, isLoggingIn, password, props, rememberMe, username]); const onSubmit2FA = useCallback(() => { setLoggingIn(true); api.verifyTotp2FA(tfaCode).then((res) => { setLoggingIn(false); if (res.success) { set2FAToken({ ...tfaToken, authenticated: true }); props.onLogin(); } else { set2FAToken({ ...tfaToken, step: 2, error: res.msg }); } }); }, [tfaCode, tfaToken, props]); const onCancel2FA = useCallback(() => { abortController.abort(); props.onLogout(); set2FAToken({authenticated: false, step: 0, error: ""}); }, [props, abortController]); useEffect(() => { if (!api.loggedIn) { return; } if (!tfaToken || !tfaToken.confirmed || tfaToken.authenticated || tfaToken.type !== "fido") { return; } let step = tfaToken.step || 0; if (step !== 0) { return; } console.log("navigator.credentials.get") set2FAToken({ ...tfaToken, step: 1, error: "" }); navigator.credentials.get({ publicKey: { challenge: encodeText(window.atob(tfaToken.challenge)), allowCredentials: [{ id: encodeText(window.atob(tfaToken.credentialID)), type: "public-key", }], userVerification: "discouraged", attestation: "direct", }, signal: abortSignal }).then((res) => { let credentialID = res.id; let clientDataJson = decodeText(res.response.clientDataJSON); let authData = window.btoa(decodeText(res.response.authenticatorData)); let signature = window.btoa(decodeText(res.response.signature)); api.verifyKey2FA(credentialID, clientDataJson, authData, signature).then((res) => { if (!res.success) { set2FAToken({ ...tfaToken, step: 2, error: res.msg }); } else { props.onLogin(); } }); }).catch(e => { set2FAToken({ ...tfaToken, step: 2, error: e.toString() }); }); }, [api.loggedIn, tfaToken, props.onLogin, props.onKey2FA, abortSignal]); const createForm = () => { // 2FA if (api.loggedIn && tfaToken.type) { if (tfaToken.type === "totp") { return <>
{L("account.2fa_title")}:
set2FACode(e.target.value)} onKeyDown={e => e.key === "Enter" && onSubmit2FA()} /> { tfaToken.error ? {tfaToken.error} : <> } } else if (tfaToken.type === "fido") { return <>
{L("account.2fa_title")}:

{L("account.2fa_text")} {tfaToken.step !== 2 ? :
{L("general.something_went_wrong")}:
{tfaToken.error}
}
} } return <> setUsername(e.target.value)} onKeyDown={e => e.key === "Enter" && passwordRef.current && passwordRef.current.focus()} /> setPassword(e.target.value)} onKeyDown={e => e.key === "Enter" && onLogin()} inputRef={passwordRef} /> } label={L("account.remember_me")} checked={rememberMe} onClick={(e) => setRememberMe(!rememberMe)} /> { error ? {error} {emailConfirmed === false ? <> Click here to resend the confirmation email. : <> } : (successMessage ? {successMessage} : <>) } {L("account.forgot_password")} { props.info.registrationAllowed ? {L("account.register_text")} : <> } } if (!loaded) { return

{L("general.loading", "Loading")}…

} let successMessage = getParameter("success"); return {"Logo"} {props.info.siteName} { createForm() } }