mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
added time handling for email login's confirmation codes
This commit is contained in:
parent
a8799095cd
commit
4ab7303071
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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,104 +128,137 @@ 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>
|
<>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<MultiStepFormElement>
|
||||||
<Grid
|
<form onSubmit={formik.handleSubmit}>
|
||||||
container
|
<Grid
|
||||||
spacing={4}
|
container
|
||||||
padding={4}
|
spacing={4}
|
||||||
justifyContent="center"
|
padding={4}
|
||||||
flexDirection="column"
|
justifyContent="center"
|
||||||
>
|
flexDirection="column"
|
||||||
<Grid item>
|
>
|
||||||
<Typography variant="h6" component="h1" align="center">
|
<Grid item>
|
||||||
{t("routes.LoginRoute.forms.confirmCode.title")}
|
<Typography variant="h6" component="h1" align="center">
|
||||||
</Typography>
|
{t("routes.LoginRoute.forms.confirmCode.title")}
|
||||||
</Grid>
|
</Typography>
|
||||||
<Grid item>
|
</Grid>
|
||||||
<Box display="flex" justifyContent="center">
|
<Grid item>
|
||||||
<MdMail size={64} />
|
<Box display="flex" justifyContent="center">
|
||||||
</Box>
|
<MdMail size={64} />
|
||||||
</Grid>
|
</Box>
|
||||||
<Grid item>
|
</Grid>
|
||||||
<Typography variant="subtitle1" component="p" align="center">
|
<Grid item>
|
||||||
{t("routes.LoginRoute.forms.confirmCode.description")}
|
<Typography variant="subtitle1" component="p" align="center">
|
||||||
</Typography>
|
{t("routes.LoginRoute.forms.confirmCode.description")}
|
||||||
</Grid>
|
</Typography>
|
||||||
<Grid item>
|
</Grid>
|
||||||
<Grid container spacing={2} direction="column">
|
<Grid item>
|
||||||
<Grid item>
|
<Grid container spacing={2} direction="column">
|
||||||
<FormControlLabel
|
<Grid item>
|
||||||
disabled={isLoading}
|
<FormControlLabel
|
||||||
control={
|
disabled={isLoading}
|
||||||
<Switch
|
control={
|
||||||
disabled={isLoading}
|
<Switch
|
||||||
checked={allowLoginFromDifferentDevices}
|
disabled={isLoading}
|
||||||
onChange={() =>
|
checked={allowLoginFromDifferentDevices}
|
||||||
changeAllowLoginFromDifferentDevice(
|
onChange={() =>
|
||||||
!allowLoginFromDifferentDevices,
|
changeAllowLoginFromDifferentDevice(
|
||||||
)
|
!allowLoginFromDifferentDevices,
|
||||||
}
|
)
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
labelPlacement="end"
|
}
|
||||||
label={t(
|
labelPlacement="end"
|
||||||
"routes.LoginRoute.forms.confirmCode.allowLoginFromDifferentDevices",
|
label={t(
|
||||||
)}
|
"routes.LoginRoute.forms.confirmCode.allowLoginFromDifferentDevices",
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
key="code"
|
||||||
|
fullWidth
|
||||||
|
name="code"
|
||||||
|
id="code"
|
||||||
|
label={t(
|
||||||
|
"routes.LoginRoute.forms.confirmCode.form.code.label",
|
||||||
|
)}
|
||||||
|
value={formik.values.code}
|
||||||
|
onChange={event => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
"code",
|
||||||
|
event.target.value.replace(/\D/g, ""),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
error={formik.touched.code && Boolean(formik.errors.code)}
|
||||||
|
helperText={formik.touched.code && formik.errors.code}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<FaHashtag />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
</Grid>
|
||||||
<TextField
|
<Grid item>
|
||||||
key="code"
|
<Grid
|
||||||
fullWidth
|
width="100%"
|
||||||
name="code"
|
container
|
||||||
id="code"
|
display="flex"
|
||||||
label={t("routes.LoginRoute.forms.confirmCode.form.code.label")}
|
justifyContent="space-between"
|
||||||
value={formik.values.code}
|
>
|
||||||
onChange={event => {
|
<Grid item>
|
||||||
formik.setFieldValue(
|
<ResendMailButton
|
||||||
"code",
|
email={email}
|
||||||
event.target.value.replace(/\D/g, ""),
|
sameRequestToken={sameRequestToken}
|
||||||
)
|
/>
|
||||||
}}
|
</Grid>
|
||||||
disabled={formik.isSubmitting}
|
<Grid item>
|
||||||
error={formik.touched.code && Boolean(formik.errors.code)}
|
<LoadingButton
|
||||||
helperText={formik.touched.code && formik.errors.code}
|
loading={formik.isSubmitting}
|
||||||
InputProps={{
|
variant="contained"
|
||||||
startAdornment: (
|
type="submit"
|
||||||
<InputAdornment position="start">
|
startIcon={<MdChevronRight />}
|
||||||
<FaHashtag />
|
>
|
||||||
</InputAdornment>
|
{t("routes.LoginRoute.forms.confirmCode.continueAction")}
|
||||||
),
|
</LoadingButton>
|
||||||
}}
|
</Grid>
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
</form>
|
||||||
<Grid width="100%" container display="flex" justifyContent="space-between">
|
</MultiStepFormElement>
|
||||||
<Grid item>
|
<Snackbar open={isExpiringSoon}>
|
||||||
<ResendMailButton
|
<Alert severity="warning" variant="filled">
|
||||||
email={email}
|
{t("routes.LoginRoute.forms.confirmCode.expiringSoon")}
|
||||||
sameRequestToken={sameRequestToken}
|
</Alert>
|
||||||
/>
|
</Snackbar>
|
||||||
</Grid>
|
</>
|
||||||
<Grid item>
|
|
||||||
<LoadingButton
|
|
||||||
loading={formik.isSubmitting}
|
|
||||||
variant="contained"
|
|
||||||
type="submit"
|
|
||||||
startIcon={<MdChevronRight />}
|
|
||||||
>
|
|
||||||
{t("routes.LoginRoute.forms.confirmCode.continueAction")}
|
|
||||||
</LoadingButton>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</form>
|
|
||||||
</MultiStepFormElement>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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}) => {
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user