added time handling for email login's confirmation codes

This commit is contained in:
Myzel394 2022-12-17 21:38:57 +01:00
parent a8799095cd
commit 4ab7303071
6 changed files with 153 additions and 99 deletions

View File

@ -40,6 +40,7 @@
"description": "Wir haben einen Code an deine E-Mail gesendet. Gib ihn hier ein, um dich anzumelden.", "description": "Wir haben einen Code an deine E-Mail gesendet. Gib ihn hier ein, um dich anzumelden.",
"continueAction": "Anmelden", "continueAction": "Anmelden",
"allowLoginFromDifferentDevices": "Anmelden von anderen Geräten erlauben", "allowLoginFromDifferentDevices": "Anmelden von anderen Geräten erlauben",
"expiringSoon": "Dein Code läuft in weniger als einer Minute ab.",
"form": { "form": {
"code": { "code": {
"label": "Verifizierungscode", "label": "Verifizierungscode",

View File

@ -40,6 +40,7 @@
"description": "We sent you a code to your email. Enter it below to login", "description": "We sent you a code to your email. Enter it below to login",
"continueAction": "Log in", "continueAction": "Log in",
"allowLoginFromDifferentDevices": "Allow login from different devices", "allowLoginFromDifferentDevices": "Allow login from different devices",
"expiringSoon": "Your code will expire in less than a minute.",
"form": { "form": {
"code": { "code": {
"label": "Verification Code", "label": "Verification Code",

View File

@ -1,18 +1,23 @@
import * as yup from "yup" import * as yup from "yup"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {ReactElement, useState} from "react" import {ReactElement, useCallback, useMemo, useState} from "react"
import {useFormik} from "formik" import {useFormik} from "formik"
import {FaHashtag} from "react-icons/fa" import {FaHashtag} from "react-icons/fa"
import {MdChevronRight, MdMail} from "react-icons/md" import {MdChevronRight, MdMail} from "react-icons/md"
import {useLoaderData} from "react-router-dom" import {useLoaderData} from "react-router-dom"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {useEffectOnce} from "react-use"
import differenceInSeconds from "date-fns/differenceInSeconds"
import inMilliseconds from "in-milliseconds"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import { import {
Alert,
Box, Box,
FormControlLabel, FormControlLabel,
Grid, Grid,
InputAdornment, InputAdornment,
Snackbar,
Switch, Switch,
TextField, TextField,
Typography, Typography,
@ -28,12 +33,14 @@ import {
import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis" import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis"
import {MultiStepFormElement} from "~/components" import {MultiStepFormElement} from "~/components"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
import {isDev} from "~/constants/development"
import changeAllowEmailLoginFromDifferentDevices from "~/apis/change-allow-email-login-from-different-devices" import changeAllowEmailLoginFromDifferentDevices from "~/apis/change-allow-email-login-from-different-devices"
import ResendMailButton from "./ResendMailButton" import ResendMailButton from "./ResendMailButton"
export interface ConfirmCodeFormProps { export interface ConfirmCodeFormProps {
onConfirm: (user: ServerUser) => void onConfirm: (user: ServerUser) => void
onCodeExpired: () => void
email: string email: string
sameRequestToken: string sameRequestToken: string
} }
@ -45,11 +52,15 @@ interface Form {
export default function ConfirmCodeForm({ export default function ConfirmCodeForm({
onConfirm, onConfirm,
onCodeExpired,
email, email,
sameRequestToken, sameRequestToken,
}: ConfirmCodeFormProps): ReactElement { }: ConfirmCodeFormProps): ReactElement {
const settings = useLoaderData() as ServerSettings const settings = useLoaderData() as ServerSettings
const expirationTime = isDev ? 9 : settings.emailLoginExpirationInSeconds
const {t} = useTranslation() const {t} = useTranslation()
const requestDate = useMemo(() => new Date(), [])
const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false)
const schema = yup.object().shape({ const schema = yup.object().shape({
code: yup code: yup
@ -117,8 +128,28 @@ export default function ConfirmCodeForm({
}, },
}, },
) )
const checkExpiration = useCallback(() => {
const diff = differenceInSeconds(new Date(), requestDate)
if (diff >= expirationTime) {
onCodeExpired()
} else if (diff >= expirationTime - 60) {
setIsExpiringSoon(true)
}
}, [requestDate])
useEffectOnce(() => {
const preCheck = setInterval(checkExpiration, inMilliseconds.seconds(isDev ? 1 : 20))
const finalCheck = setTimeout(checkExpiration, inMilliseconds.seconds(expirationTime))
return () => {
clearInterval(preCheck)
clearTimeout(finalCheck)
}
})
return ( return (
<>
<MultiStepFormElement> <MultiStepFormElement>
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<Grid <Grid
@ -171,7 +202,9 @@ export default function ConfirmCodeForm({
fullWidth fullWidth
name="code" name="code"
id="code" id="code"
label={t("routes.LoginRoute.forms.confirmCode.form.code.label")} label={t(
"routes.LoginRoute.forms.confirmCode.form.code.label",
)}
value={formik.values.code} value={formik.values.code}
onChange={event => { onChange={event => {
formik.setFieldValue( formik.setFieldValue(
@ -194,7 +227,12 @@ export default function ConfirmCodeForm({
</Grid> </Grid>
</Grid> </Grid>
<Grid item> <Grid item>
<Grid width="100%" container display="flex" justifyContent="space-between"> <Grid
width="100%"
container
display="flex"
justifyContent="space-between"
>
<Grid item> <Grid item>
<ResendMailButton <ResendMailButton
email={email} email={email}
@ -216,5 +254,11 @@ export default function ConfirmCodeForm({
</Grid> </Grid>
</form> </form>
</MultiStepFormElement> </MultiStepFormElement>
<Snackbar open={isExpiringSoon}>
<Alert severity="warning" variant="filled">
{t("routes.LoginRoute.forms.confirmCode.expiringSoon")}
</Alert>
</Snackbar>
</>
) )
} }

View File

@ -14,6 +14,7 @@ import {MultiStepFormElement, SimpleForm} from "~/components"
import {useExtensionHandler} from "~/hooks" import {useExtensionHandler} from "~/hooks"
export interface EmailFormProps { export interface EmailFormProps {
email: string
onLogin: (email: string, sameRequestToken: string) => void onLogin: (email: string, sameRequestToken: string) => void
} }
@ -22,7 +23,7 @@ interface Form {
detail: string detail: string
} }
export default function EmailForm({onLogin}: EmailFormProps): ReactElement { export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const $password = useRef<HTMLInputElement | null>(null) const $password = useRef<HTMLInputElement | null>(null)
@ -40,7 +41,7 @@ export default function EmailForm({onLogin}: EmailFormProps): ReactElement {
const formik = useFormik<Form>({ const formik = useFormik<Form>({
validationSchema: schema, validationSchema: schema,
initialValues: { initialValues: {
email: "", email: preFilledEmail,
detail: "", detail: "",
}, },
onSubmit: async (values, {setErrors}) => { onSubmit: async (values, {setErrors}) => {

View File

@ -14,6 +14,7 @@ export default function LoginRoute(): ReactElement {
const {token, email: queryEmail} = useQueryParams<{token: string; email: string}>() const {token, email: queryEmail} = useQueryParams<{token: string; email: string}>()
const {login, user} = useContext(AuthContext) const {login, user} = useContext(AuthContext)
const [step, setStep] = useState<number>(0)
const [email, setEmail] = useState<string>("") const [email, setEmail] = useState<string>("")
const [sameRequestToken, setSameRequestToken] = useState<string>("") const [sameRequestToken, setSameRequestToken] = useState<string>("")
@ -38,19 +39,24 @@ export default function LoginRoute(): ReactElement {
steps={[ steps={[
<EmailForm <EmailForm
key="email_form" key="email_form"
email={email}
onLogin={(email, sameRequestToken) => { onLogin={(email, sameRequestToken) => {
setEmail(email) setEmail(email)
setStep(1)
setSameRequestToken(sameRequestToken) setSameRequestToken(sameRequestToken)
}} }}
/>, />,
<ConfirmCodeForm <ConfirmCodeForm
key="confirm_code_form" key={`confirm_code_form:${email}:${step}`}
onConfirm={login}
email={email} email={email}
sameRequestToken={sameRequestToken} sameRequestToken={sameRequestToken}
onConfirm={login}
onCodeExpired={() => {
setStep(0)
}}
/>, />,
]} ]}
index={email === "" ? 0 : 1} index={step}
/> />
) )
} }

View File

@ -77,6 +77,7 @@ export interface ServerSettings {
emailLoginTokenChars: string emailLoginTokenChars: string
emailLoginTokenLength: number emailLoginTokenLength: number
emailResendWaitTime: number emailResendWaitTime: number
emailLoginExpirationInSeconds: number
customAliasSuffixLength: number customAliasSuffixLength: number
} }