feat: Add recover 2fa

This commit is contained in:
Myzel394 2023-02-26 18:20:19 +01:00
parent 74e8ad2c7b
commit b1a1a25bf7
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
4 changed files with 142 additions and 0 deletions

View File

@ -104,6 +104,19 @@
}
}
},
"Recover2FARoute": {
"title": "Recover Two-Factor-Authentication",
"description": "We are very sorry if you lost your codes. Please enter a recovery code to continue. Note that this will disable two-factor authentication for your account. You can enable it again in the settings.",
"forms": {
"recoveryCode": {
"label": "Recovery Code"
},
"submit": "Disable 2FA"
},
"unauthorized": "Please make sure to log in first and then reset your two-factor authentication on its screen.",
"canLoginNow": "Two-factor authentication has been disabled. You can now log in.",
"loggedIn": "Two-factor authentication has been disabled. You are now logged in."
},
"CompleteAccountRoute": {
"forms": {
"generateReports": {

View File

@ -20,6 +20,7 @@ import GlobalSettingsRoute from "~/routes/GlobalSettingsRoute"
import I18nHandler from "./I18nHandler"
import LoginRoute from "~/routes/LoginRoute"
import LogoutRoute from "~/routes/LogoutRoute"
import Recover2FARoute from "./routes/Recover2FARoute"
import RedirectRoute from "./routes/RedirectRoute"
import ReportDetailRoute from "~/routes/ReportDetailRoute"
import ReportsRoute from "~/routes/ReportsRoute"
@ -53,6 +54,10 @@ const router = createBrowserRouter([
loader: getServerSettings,
element: <LoginRoute />,
},
{
path: "/auth/recover-2fa",
element: <Recover2FARoute />,
},
{
path: "/auth/signup",
loader: getServerSettings,

View File

@ -96,6 +96,7 @@ export default function OTPForm({
</Grid>
<Grid item>
<TextField
autoFocus
key="code"
fullWidth
name="code"

View File

@ -0,0 +1,123 @@
import * as yup from "yup"
import {ReactElement, useContext} from "react"
import {AxiosError} from "axios"
import {useTranslation} from "react-i18next"
import {BsShieldLockFill} from "react-icons/bs"
import {useFormik} from "formik"
import {useNavigate} from "react-router-dom"
import {useMutation} from "@tanstack/react-query"
import {LoadingButton} from "@mui/lab"
import {Box, Grid, Paper, TextField, Typography} from "@mui/material"
import {SimpleDetailResponse} from "~/server-types"
import {Delete2FAData, delete2FA, getMe} from "~/apis"
import {useErrorSuccessSnacks} from "~/hooks"
import {AuthContext} from "~/components"
interface Form {
recoveryCode: string
}
export default function Recover2FARoute(): ReactElement {
const {t} = useTranslation()
const {showError, showSuccess} = useErrorSuccessSnacks()
const {login} = useContext(AuthContext)
const navigate = useNavigate()
const {mutateAsync} = useMutation<SimpleDetailResponse, AxiosError, Delete2FAData>(delete2FA, {
onSuccess: async () => {
try {
const user = await getMe()
showSuccess(t("routes.Recover2FARoute.loggedIn").toString())
login(user)
navigate("/aliases")
} catch (error) {
showSuccess(t("routes.Recover2FARoute.canLoginNow").toString())
navigate("/auth/login")
}
},
onError: error => {
if (error.response?.status == 401) {
showError(t("routes.Recover2FARoute.unauthorized").toString())
navigate("/auth/login")
} else {
showError(error)
}
},
})
const schema = yup.object().shape({
recoveryCode: yup.string().required().label(t("routes.LoginRoute.forms.otp.code.label")),
})
const formik = useFormik<Form>({
validationSchema: schema,
initialValues: {
recoveryCode: "",
},
onSubmit: async values =>
mutateAsync({
recoveryCode: values.recoveryCode.replaceAll("-", ""),
}),
})
return (
<Paper>
<Box maxWidth="sm">
<form onSubmit={formik.handleSubmit}>
<Grid
container
spacing={4}
padding={4}
alignItems="center"
justifyContent="center"
flexDirection="column"
>
<Grid item>
<Typography variant="h5" component="h1" align="center">
{t("routes.Recover2FARoute.title")}
</Typography>
</Grid>
<Grid item>
<Box display="flex" justifyContent="center">
<BsShieldLockFill size={64} />
</Box>
</Grid>
<Grid item>
<Typography variant="body1" align="center">
{t("routes.Recover2FARoute.description")}
</Typography>
</Grid>
<Grid item>
<TextField
autoFocus
key="recoveryCode"
fullWidth
name="recoveryCode"
id="recoveryCode"
label={t("routes.Recover2FARoute.forms.recoveryCode.label")}
value={formik.values.recoveryCode}
onChange={formik.handleChange}
error={Boolean(
formik.touched.recoveryCode && formik.errors.recoveryCode,
)}
helperText={formik.errors.recoveryCode}
/>
</Grid>
<Grid item>
<LoadingButton
type="submit"
variant="contained"
loading={formik.isSubmitting}
startIcon={<BsShieldLockFill />}
>
{t("routes.Recover2FARoute.forms.submit")}
</LoadingButton>
</Grid>
</Grid>
</form>
</Box>
</Paper>
)
}