added ability to login from different devices

This commit is contained in:
Myzel394 2022-11-04 21:48:17 +01:00
parent ccdaf12ee3
commit 8ae24aec8e
6 changed files with 164 additions and 24 deletions

View File

@ -35,6 +35,7 @@
"title": "You got mail!", "title": "You got mail!",
"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",
"form": { "form": {
"code": { "code": {
"label": "Verification Code", "label": "Verification Code",
@ -43,6 +44,10 @@
} }
} }
} }
},
"confirmFromDifferentDevice": {
"title": "Login failed",
"description": "You could not be logged in. This could either be because you are not allowed to login from different devices or the verification code is invalid or expired."
} }
} }
}, },

View File

@ -15,7 +15,7 @@ export default async function changeAllowEmailLoginFromDifferentDevices({
const {data} = await client.patch( const {data} = await client.patch(
`${ `${
import.meta.env.VITE_SERVER_BASE_URL import.meta.env.VITE_SERVER_BASE_URL
}/auth/login/email-token/allow-email-login-from-different-devices`, }/auth/login/email-token/allow-login-from-different-devices`,
{ {
email, email,
sameRequestToken, sameRequestToken,

View File

@ -5,7 +5,7 @@ import parseUser from "~/apis/helpers/parse-user"
export interface VerifyLoginWithEmailData { export interface VerifyLoginWithEmailData {
email: string email: string
token: string token: string
sameRequestToken: string sameRequestToken?: string
} }
export default async function verifyLoginWithEmail({ export default async function verifyLoginWithEmail({

View File

@ -1,6 +1,6 @@
import * as yup from "yup" import * as yup from "yup"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {ReactElement} from "react" import {ReactElement, 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"
@ -8,13 +8,27 @@ import {useLoaderData} from "react-router-dom"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {Box, Grid, InputAdornment, TextField, Typography} from "@mui/material" import {
Box,
FormControlLabel,
Grid,
InputAdornment,
Switch,
TextField,
Typography,
} from "@mui/material"
import {LoadingButton} from "@mui/lab" import {LoadingButton} from "@mui/lab"
import {AuthenticationDetails, ServerSettings, ServerUser} from "~/server-types" import {
AuthenticationDetails,
ServerSettings,
ServerUser,
SimpleDetailResponse,
} from "~/server-types"
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 changeAllowEmailLoginFromDifferentDevices from "~/apis/change-allow-email-login-from-different-devices"
import ResendMailButton from "./ResendMailButton" import ResendMailButton from "./ResendMailButton"
@ -84,6 +98,25 @@ export default function ConfirmCodeForm({
} }
}, },
}) })
const [allowLoginFromDifferentDevices, setAllowLoginFromDifferentDevices] =
useState<boolean>(false)
const {mutateAsync: changeAllowLoginFromDifferentDevice, isLoading} = useMutation<
SimpleDetailResponse,
AxiosError,
boolean
>(
allow =>
changeAllowEmailLoginFromDifferentDevices({
allow,
email,
sameRequestToken,
}),
{
onSuccess: (_, allow) => {
setAllowLoginFromDifferentDevices(allow)
},
},
)
return ( return (
<MultiStepFormElement> <MultiStepFormElement>
@ -111,25 +144,49 @@ export default function ConfirmCodeForm({
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<TextField <Grid container spacing={2} direction="column">
key="code" <Grid item>
fullWidth <FormControlLabel
name="code" disabled={isLoading}
id="code" control={
label={t("routes.LoginRoute.forms.confirmCode.form.code.label")} <Switch
value={formik.values.code} disabled={isLoading}
onChange={formik.handleChange} checked={allowLoginFromDifferentDevices}
disabled={formik.isSubmitting} onChange={() =>
error={formik.touched.code && Boolean(formik.errors.code)} changeAllowLoginFromDifferentDevice(
helperText={formik.touched.code && formik.errors.code} !allowLoginFromDifferentDevices,
InputProps={{ )
startAdornment: ( }
<InputAdornment position="start"> />
<FaHashtag /> }
</InputAdornment> labelPlacement="end"
), 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={formik.handleChange}
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>
<Grid item> <Grid item>
<Grid width="100%" container display="flex" justifyContent="space-between"> <Grid width="100%" container display="flex" justifyContent="space-between">

View File

@ -0,0 +1,71 @@
import {ReactElement} from "react"
import {AxiosError} from "axios"
import {useMount} from "react-use"
import {useTranslation} from "react-i18next"
import {useMutation} from "@tanstack/react-query"
import {Box, Grid, Paper, Typography} from "@mui/material"
import {AuthenticationDetails, ServerUser} from "~/server-types"
import {verifyLoginWithEmail} from "~/apis"
import LoadingData from "~/components/LoadingData"
export interface ConfirmFromDifferentDeviceProps {
email: string
token: string
onConfirm: (user: ServerUser) => void
}
export default function ConfirmFromDifferentDevice({
email,
token,
onConfirm,
}: ConfirmFromDifferentDeviceProps): ReactElement {
const {t} = useTranslation()
const {mutate, isLoading, isError} = useMutation<AuthenticationDetails, AxiosError, void>(
() =>
verifyLoginWithEmail({
email,
token,
}),
{
onSuccess: ({user}) => onConfirm(user),
},
)
useMount(mutate)
if (isLoading) {
return (
<Paper>
<LoadingData />
</Paper>
)
}
if (isError) {
return (
<Paper>
<Box padding={4}>
<Grid container spacing={2} direction="column" alignItems="center">
<Grid item>
<Typography variant="h6" component="h1">
{t("routes.LoginRoute.forms.confirmFromDifferentDevice.title")}
</Typography>
</Grid>
<Grid item>
<Typography variant="body1">
{t(
"routes.LoginRoute.forms.confirmFromDifferentDevice.description",
)}
</Typography>
</Grid>
</Grid>
</Box>
</Paper>
)
}
return <></>
}

View File

@ -3,12 +3,15 @@ import {useNavigate} from "react-router-dom"
import {useUpdateEffect} from "react-use" import {useUpdateEffect} from "react-use"
import {MultiStepForm} from "~/components" import {MultiStepForm} from "~/components"
import {useQueryParams} from "~/hooks"
import AuthContext from "~/AuthContext/AuthContext" import AuthContext from "~/AuthContext/AuthContext"
import ConfirmCodeForm from "~/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm" import ConfirmCodeForm from "~/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm"
import ConfirmFromDifferentDevice from "~/route-widgets/LoginRoute/ConfirmFromDifferentDevice"
import EmailForm from "~/route-widgets/LoginRoute/EmailForm" import EmailForm from "~/route-widgets/LoginRoute/EmailForm"
export default function LoginRoute(): ReactElement { export default function LoginRoute(): ReactElement {
const navigate = useNavigate() const navigate = useNavigate()
const {token, email: queryEmail} = useQueryParams<{token: string; email: string}>()
const {login, user} = useContext(AuthContext) const {login, user} = useContext(AuthContext)
const [email, setEmail] = useState<string>("") const [email, setEmail] = useState<string>("")
@ -26,6 +29,10 @@ export default function LoginRoute(): ReactElement {
} }
}, [user?.encryptedPassword]) }, [user?.encryptedPassword])
if (token && queryEmail) {
return <ConfirmFromDifferentDevice email={queryEmail} token={token} onConfirm={login} />
}
return ( return (
<MultiStepForm <MultiStepForm
steps={[ steps={[