lamassu-server/packages/admin-ui/src/pages/Authentication/Reset2FA.jsx
2025-05-12 14:55:22 +01:00

204 lines
6.2 KiB
JavaScript

import { useQuery, useMutation, gql } from '@apollo/client'
import Grid from '@mui/material/Grid'
import Paper from '@mui/material/Paper'
import { Form, Formik } from 'formik'
import { QRCodeSVG as QRCode } from 'qrcode.react'
import React, { useReducer, useState } from 'react'
import { useLocation, useHistory } from 'react-router-dom'
import { H2, Label2, Label3, P } from 'src/components/typography'
import Logo from 'src/styling/icons/menu/logo.svg?react'
import { ActionButton, Button } from 'src/components/buttons'
import { CodeInput } from 'src/components/inputs/base'
import { primaryColor } from 'src/styling/variables'
import classes from './Authentication.module.css'
const VALIDATE_RESET_2FA_LINK = gql`
query validateReset2FALink($token: String!) {
validateReset2FALink(token: $token) {
user_id
secret
otpauth
}
}
`
const RESET_2FA = gql`
mutation reset2FA($token: String!, $userID: ID!, $code: String!) {
reset2FA(token: $token, userID: $userID, code: $code)
}
`
const initialState = {
userID: null,
secret: null,
otpauth: null,
result: null,
}
const reducer = (state, action) => {
const { type, payload } = action
return { ...state, ...payload, result: type }
}
const Reset2FA = () => {
const history = useHistory()
const QueryParams = () => new URLSearchParams(useLocation().search)
const token = QueryParams().get('t')
const [isShowing, setShowing] = useState(false)
const [invalidToken, setInvalidToken] = useState(false)
const [twoFAConfirmation, setTwoFAConfirmation] = useState('')
const [state, dispatch] = useReducer(reducer, initialState)
const handle2FAChange = value => {
setTwoFAConfirmation(value)
setInvalidToken(false)
}
const { error: queryError, loading } = useQuery(VALIDATE_RESET_2FA_LINK, {
variables: { token: token },
onCompleted: ({ validateReset2FALink: info }) => {
if (!info) {
dispatch({
type: 'failure',
})
} else {
dispatch({
type: 'success',
payload: {
userID: info.user_id,
secret: info.secret,
otpauth: info.otpauth,
},
})
}
},
onError: () => {
dispatch({
type: 'failure',
})
},
})
const [reset2FA, { error: mutationError }] = useMutation(RESET_2FA, {
onCompleted: ({ reset2FA: success }) => {
success ? history.push('/') : setInvalidToken(true)
},
})
const getErrorMsg = () => {
if (queryError) return 'Internal server error'
if (twoFAConfirmation.length !== 6 && invalidToken)
return 'The code should have 6 characters!'
if (mutationError || invalidToken)
return 'Code is invalid. Please try again.'
return null
}
const handleSubmit = () => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
reset2FA({
variables: {
token: token,
userID: state.userID,
code: twoFAConfirmation,
},
})
}
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justifyContent="center"
className={classes.welcomeBackground}>
<Grid>
<div>
<Paper elevation={1}>
<div className={classes.wrapper}>
<div className={classes.titleWrapper}>
<Logo className={classes.icon} />
<H2 className={classes.title}>Lamassu Admin</H2>
</div>
{!loading && state.result === 'success' && (
<>
<div className={classes.infoWrapper}>
<Label2 className={classes.info2}>
To finish this process, please scan the following QR code
or insert the secret further below on an authentication
app of your choice, such Google Authenticator or Authy.
</Label2>
</div>
<div className={classes.qrCodeWrapper}>
<QRCode
size={240}
fgColor={primaryColor}
value={state.otpauth}
/>
</div>
<div className={classes.secretWrapper}>
<Label2 className={classes.secretLabel}>
Your secret:
</Label2>
<Label2
className={
isShowing ? classes.secret : classes.hiddenSecret
}>
{state.secret}
</Label2>
<ActionButton
color="primary"
onClick={() => {
setShowing(!isShowing)
}}>
{isShowing ? 'Hide' : 'Show'}
</ActionButton>
</div>
<div className={classes.confirm2FAInput}>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput
name="2fa"
value={twoFAConfirmation}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
/>
<div className="mt-9">
{getErrorMsg() && (
<P className="text-tomato">{getErrorMsg()}</P>
)}
<Button
onClick={handleSubmit}
buttonClassName="w-full">
Done
</Button>
</div>
</Form>
</Formik>
</div>
</>
)}
{!loading && state.result === 'failure' && (
<>
<Label3>Link has expired</Label3>
</>
)}
</div>
</Paper>
</div>
</Grid>
</Grid>
)
}
export default Reset2FA