231 lines
6 KiB
JavaScript
231 lines
6 KiB
JavaScript
import { useMutation, useLazyQuery, gql } from '@apollo/client'
|
|
import { startAssertion } from '@simplewebauthn/browser'
|
|
import { Field, Form, Formik } from 'formik'
|
|
import React, { useContext } from 'react'
|
|
import { useLocation } from 'wouter'
|
|
import { Label3, P } from '../../components/typography'
|
|
import * as Yup from 'yup'
|
|
|
|
import AppContext from '../../AppContext'
|
|
import { Button } from '../../components/buttons'
|
|
import {
|
|
Checkbox,
|
|
SecretInput,
|
|
TextInput,
|
|
} from '../../components/inputs/formik'
|
|
|
|
const LOGIN = gql`
|
|
mutation login($username: String!, $password: String!) {
|
|
login(username: $username, password: $password)
|
|
}
|
|
`
|
|
|
|
const GENERATE_ASSERTION = gql`
|
|
query generateAssertionOptions($domain: String!) {
|
|
generateAssertionOptions(domain: $domain)
|
|
}
|
|
`
|
|
|
|
const VALIDATE_ASSERTION = gql`
|
|
mutation validateAssertion(
|
|
$assertionResponse: JSONObject!
|
|
$domain: String!
|
|
) {
|
|
validateAssertion(assertionResponse: $assertionResponse, domain: $domain)
|
|
}
|
|
`
|
|
|
|
const GET_USER_DATA = gql`
|
|
{
|
|
userData {
|
|
id
|
|
username
|
|
role
|
|
}
|
|
}
|
|
`
|
|
|
|
const validationSchema = Yup.object().shape({
|
|
email: Yup.string().label('Email').required().email(),
|
|
password: Yup.string().required('Password field is required'),
|
|
rememberMe: Yup.boolean(),
|
|
})
|
|
|
|
const initialValues = {
|
|
email: '',
|
|
password: '',
|
|
rememberMe: false,
|
|
}
|
|
|
|
const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
|
if (!formikErrors || !formikTouched) return null
|
|
if (mutationError) return 'Invalid email/password combination'
|
|
if (formikErrors.email && formikTouched.email) return formikErrors.email
|
|
if (formikErrors.password && formikTouched.password)
|
|
return formikErrors.password
|
|
return null
|
|
}
|
|
|
|
const LoginState = ({ dispatch, strategy }) => {
|
|
const [, navigate] = useLocation()
|
|
const { setUserData } = useContext(AppContext)
|
|
|
|
const [login, { error: loginMutationError }] = useMutation(LOGIN)
|
|
|
|
const submitLogin = async (username, password, rememberMe) => {
|
|
const options = {
|
|
variables: {
|
|
username,
|
|
password,
|
|
},
|
|
}
|
|
const { data: loginResponse } = await login(options)
|
|
|
|
if (!loginResponse.login) return
|
|
|
|
return dispatch({
|
|
type: loginResponse.login,
|
|
payload: {
|
|
clientField: username,
|
|
passwordField: password,
|
|
rememberMeField: rememberMe,
|
|
},
|
|
})
|
|
}
|
|
|
|
const [validateAssertion, { error: FIDOMutationError }] = useMutation(
|
|
VALIDATE_ASSERTION,
|
|
{
|
|
onCompleted: ({ validateAssertion: success }) => success && getUserData(),
|
|
},
|
|
)
|
|
|
|
const [assertionOptions, { error: assertionQueryError }] = useLazyQuery(
|
|
GENERATE_ASSERTION,
|
|
{
|
|
onCompleted: ({ generateAssertionOptions: options }) => {
|
|
startAssertion(options)
|
|
.then(res => {
|
|
validateAssertion({
|
|
variables: {
|
|
assertionResponse: res,
|
|
domain: window.location.hostname,
|
|
},
|
|
})
|
|
})
|
|
.catch(err => {
|
|
console.error(err)
|
|
})
|
|
},
|
|
},
|
|
)
|
|
|
|
const [getUserData, { error: userDataQueryError }] = useLazyQuery(
|
|
GET_USER_DATA,
|
|
{
|
|
onCompleted: ({ userData }) => {
|
|
setUserData(userData)
|
|
navigate('/')
|
|
},
|
|
},
|
|
)
|
|
|
|
return (
|
|
<Formik
|
|
validationSchema={validationSchema}
|
|
initialValues={initialValues}
|
|
onSubmit={values =>
|
|
submitLogin(values.email, values.password, values.rememberMe)
|
|
}>
|
|
{({ errors, touched }) => (
|
|
<Form id="login-form">
|
|
<Field
|
|
name="email"
|
|
label="Email"
|
|
size="lg"
|
|
component={TextInput}
|
|
fullWidth
|
|
autoFocus
|
|
className="-mt-4 mb-6"
|
|
error={getErrorMsg(
|
|
errors,
|
|
touched,
|
|
loginMutationError ||
|
|
FIDOMutationError ||
|
|
assertionQueryError ||
|
|
userDataQueryError,
|
|
)}
|
|
/>
|
|
<Field
|
|
name="password"
|
|
size="lg"
|
|
component={SecretInput}
|
|
label="Password"
|
|
fullWidth
|
|
error={getErrorMsg(
|
|
errors,
|
|
touched,
|
|
loginMutationError ||
|
|
FIDOMutationError ||
|
|
assertionQueryError ||
|
|
userDataQueryError,
|
|
)}
|
|
/>
|
|
<div className="mt-9 flex">
|
|
<Field
|
|
name="rememberMe"
|
|
className="-ml-2 transform-[scale(1.5)]"
|
|
component={Checkbox}
|
|
size="medium"
|
|
/>
|
|
<Label3>Keep me logged in</Label3>
|
|
</div>
|
|
<div className="mt-15">
|
|
{getErrorMsg(
|
|
errors,
|
|
touched,
|
|
loginMutationError ||
|
|
FIDOMutationError ||
|
|
assertionQueryError ||
|
|
userDataQueryError,
|
|
) && (
|
|
<P className="text-tomato">
|
|
{getErrorMsg(
|
|
errors,
|
|
touched,
|
|
loginMutationError ||
|
|
FIDOMutationError ||
|
|
assertionQueryError ||
|
|
userDataQueryError,
|
|
)}
|
|
</P>
|
|
)}
|
|
{strategy !== 'FIDO2FA' && (
|
|
<Button
|
|
type="button"
|
|
onClick={() => {
|
|
return strategy === 'FIDOUsernameless'
|
|
? assertionOptions({
|
|
variables: { domain: window.location.hostname },
|
|
})
|
|
: dispatch({
|
|
type: 'FIDO',
|
|
payload: {},
|
|
})
|
|
}}
|
|
buttonClassName="w-full"
|
|
className="mb-3">
|
|
I have a hardware key
|
|
</Button>
|
|
)}
|
|
<Button type="submit" form="login-form" buttonClassName="w-full">
|
|
Login
|
|
</Button>
|
|
</div>
|
|
</Form>
|
|
)}
|
|
</Formik>
|
|
)
|
|
}
|
|
|
|
export default LoginState
|