lamassu-server/packages/admin-ui/src/pages/Authentication/LoginState.jsx
2025-05-15 13:00:21 +01:00

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