import { Box, Button, Checkbox, CircularProgress, Container, FormControlLabel, Grid, Link, TextField, Typography } from "@material-ui/core"; import {makeStyles} from '@material-ui/core/styles'; import {Alert} from '@material-ui/lab'; import React, {useCallback, useContext, useEffect, useState} from "react"; import ReplayIcon from '@material-ui/icons/Replay'; import LanguageSelection from "../elements/language-selection"; import {decodeText, encodeText, getParameter, removeParameter} from "shared/util"; import {LocaleContext} from "shared/locale"; const useStyles = makeStyles((theme) => ({ paper: { marginTop: theme.spacing(8), display: 'flex', flexDirection: 'column', alignItems: 'center', }, avatar: { margin: theme.spacing(2), width: "60px", height: "60px" }, form: { width: '100%', // Fix IE 11 issue. marginTop: theme.spacing(1), }, submit: { margin: theme.spacing(3, 0, 2), }, logo: { marginRight: theme.spacing(3) }, headline: { width: "100%", }, container: { marginTop: theme.spacing(5), paddingBottom: theme.spacing(1), borderColor: theme.palette.primary.main, borderStyle: "solid", borderWidth: 1, borderRadius: 5 }, buttons2FA: { marginTop: theme.spacing(1), marginBottom: theme.spacing(1), }, error2FA: { marginTop: theme.spacing(2), marginBottom: theme.spacing(2), "& > div": { fontSize: 16 }, "& > button": { marginTop: theme.spacing(1) } } })); export default function LoginForm(props) { const api = props.api; const classes = useStyles(); // 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); 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; } 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", }, 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)} /> { 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)} /> setPassword(e.target.value)} /> } 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}
e.preventDefault()}> { createForm() }
}