fix: multiple small fixes across auth

This commit is contained in:
Sérgio Salgado 2021-04-16 05:53:22 +01:00 committed by Josh Harvey
parent 9fa97725ec
commit bbc37c0202
22 changed files with 296 additions and 291 deletions

View file

@ -51,7 +51,7 @@ const apolloServer = new ApolloServer({
console.log(error)
return error
},
context: async ({ req }) => {
context: async ({ req, res }) => {
if (!req.session.user) return { req }
const user = await users.verifyAndUpdateUser(
@ -67,6 +67,9 @@ const apolloServer = new ApolloServer({
req.session.user.id = user.id
req.session.user.role = user.role
res.set('role', user.role)
res.set('Access-Control-Expose-Headers', 'role')
return { req }
}
})

View file

@ -21,8 +21,9 @@ function authenticateUser(username, password) {
if (!isMatch) throw new authErrors.InvalidCredentialsError()
return loginHelper.validateUser(username, hashedPassword)
})
.catch(e => {
console.error(e)
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
return user
})
}
@ -70,7 +71,7 @@ const validateRegisterLink = token => {
const validateResetPasswordLink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validatePasswordResetToken(token)
return users.validateAuthToken(token, 'reset_password')
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return { id: r.userID }
@ -80,7 +81,7 @@ const validateResetPasswordLink = token => {
const validateReset2FALink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validate2FAResetToken(token)
return users.validateAuthToken(token, 'reset_twofa')
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return users.getUserById(r.userID)
@ -111,10 +112,12 @@ const login = (username, password) => {
}
const input2FA = (username, password, rememberMe, code, context) => {
return authenticateUser(username, password).then(user => {
return authenticateUser(username, password)
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
return users.getUserById(user.id).then(user => {
return users.getUserById(user.id)
})
.then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
@ -125,17 +128,24 @@ const input2FA = (username, password, rememberMe, code, context) => {
return true
})
})
}
const setup2FA = (username, password, secret, codeConfirmation) => {
return authenticateUser(username, password).then(user => {
const setup2FA = (username, password, rememberMe, secret, codeConfirmation, context) => {
return authenticateUser(username, password)
.then(user => {
if (!user || !secret) throw new authErrors.InvalidCredentialsError()
const isCodeValid = otplib.authenticator.verify({ token: codeConfirmation, secret: secret })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
return users.save2FASecret(user.id, secret).then(() => true)
return users.getUserById(user.id)
})
.then(user => {
const finalUser = { id: user.id, username: user.username, role: user.role }
context.req.session.user = finalUser
if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE
return true
})
}
@ -143,7 +153,7 @@ const createResetPasswordToken = userID => {
return users.getUserById(userID)
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
return users.createResetPasswordToken(user.id)
return users.createAuthToken(user.id, 'reset_password')
})
.catch(err => console.error(err))
}
@ -152,7 +162,7 @@ const createReset2FAToken = userID => {
return users.getUserById(userID)
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
return users.createReset2FAToken(user.id)
return users.createAuthToken(user.id, 'reset_twofa')
})
.catch(err => console.error(err))
}
@ -177,20 +187,26 @@ const register = (token, username, password, role) => {
}
const resetPassword = (token, userID, newPassword, context) => {
return users.getUserById(userID).then(user => {
return users.getUserById(userID)
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy()
return users.updatePassword(token, user.id, newPassword)
}).then(() => true).catch(err => console.error(err))
})
.then(() => true)
.catch(err => console.error(err))
}
const reset2FA = (token, userID, code, secret, context) => {
return users.getUserById(userID).then(user => {
const isCodeValid = otplib.authenticator.verify({ token: code, secret })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
return users.getUserById(userID)
.then(user => {
if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy()
return users.reset2FASecret(token, user.id, secret).then(() => true)
}).catch(err => console.error(err))
})
.catch(err => console.error(err))
}
module.exports = {

View file

@ -7,9 +7,9 @@ const resolver = {
users: () => users.getUsers(),
sessions: () => sessionManager.getSessions(),
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username),
userData: (root, args, context, info) => authentication.getUserData(context),
userData: (...[, {}, context]) => authentication.getUserData(context),
get2FASecret: (...[, { username, password }]) => authentication.get2FASecret(username, password),
confirm2FA: (root, args, context, info) => authentication.confirm2FA(args.code, context),
confirm2FA: (...[, { code }, context]) => authentication.confirm2FA(code, context),
validateRegisterLink: (...[, { token }]) => authentication.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) => authentication.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token)
@ -17,18 +17,18 @@ const resolver = {
Mutation: {
enableUser: (...[, { id }]) => users.enableUser(id),
disableUser: (...[, { id }]) => users.disableUser(id),
deleteSession: (root, args, context, info) => authentication.deleteSession(args.sid, context),
deleteSession: (...[, { sid }, context]) => authentication.deleteSession(sid, context),
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole),
login: (...[, { username, password }]) => authentication.login(username, password),
input2FA: (root, args, context, info) => authentication.input2FA(args.username, args.password, args.rememberMe, args.code, context),
setup2FA: (...[, { username, password, secret, codeConfirmation }]) => authentication.setup2FA(username, password, secret, codeConfirmation),
input2FA: (...[, { username, password, rememberMe, code }, context]) => authentication.input2FA(username, password, rememberMe, code, context),
setup2FA: (...[, { username, password, rememberMe, secret, codeConfirmation }, context]) => authentication.setup2FA(username, password, rememberMe, secret, codeConfirmation, context),
createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID),
createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID),
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) => authentication.register(token, username, password, role),
resetPassword: (root, args, context, info) => authentication.resetPassword(args.token, args.userID, args.newPassword, context),
reset2FA: (root, args, context, info) => authentication.reset2FA(args.token, args.userID, args.code, args.secret, context)
resetPassword: (...[, { token, userID, newPassword }, context]) => authentication.resetPassword(token, userID, newPassword, context),
reset2FA: (...[, { token, userID, code, secret }, context]) => authentication.reset2FA(token, userID, code, secret, context)
}
}

View file

@ -65,7 +65,7 @@ const typeDef = `
toggleUserEnable(id: ID!): User @auth(requires: [SUPERUSER])
login(username: String!, password: String!): String
input2FA(username: String!, password: String!, code: String!, rememberMe: Boolean!): Boolean
setup2FA(username: String!, password: String!, secret: String!, codeConfirmation: String!): Boolean
setup2FA(username: String!, password: String!, rememberMe: Boolean!, secret: String!, codeConfirmation: String!): Boolean
createResetPasswordToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER])

View file

@ -4,7 +4,7 @@ function validateUser (username, password) {
const sql = 'SELECT id, username FROM users WHERE username=$1 AND password=$2'
const sqlUpdateLastAccessed = 'UPDATE users SET last_accessed = now() WHERE username=$1'
return db.oneOrNone(sql, [username, password])
return db.one(sql, [username, password])
.then(user => {
return db.none(sqlUpdateLastAccessed, [user.username])
.then(() => user)

View file

@ -72,16 +72,16 @@ function save2FASecret (id, secret) {
})
}
function validate2FAResetToken (token) {
function validateAuthToken (token, type) {
const sql = `SELECT user_id, now() < expire AS success FROM auth_tokens
WHERE token=$1 AND type='reset_twofa'`
WHERE token=$1 AND type=$2`
return db.one(sql, [token])
return db.one(sql, [token, type])
.then(res => ({ userID: res.user_id, success: res.success }))
}
function reset2FASecret (token, id, secret) {
return validate2FAResetToken(token).then(res => {
return validateAuthToken(token, 'reset_twofa').then(res => {
if (!res.success) throw new Error('Failed to verify 2FA reset token')
return db.tx(t => {
const q1 = t.none('UPDATE users SET twofa_code=$1 WHERE id=$2', [secret, id])
@ -92,23 +92,15 @@ function reset2FASecret (token, id, secret) {
})
}
function createReset2FAToken (userID) {
function createAuthToken (userID, type) {
const token = crypto.randomBytes(32).toString('hex')
const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, 'reset_twofa', $2) ON CONFLICT (user_id, type) DO UPDATE SET token=$1, expire=now() + interval '30 minutes' RETURNING *`
const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, $2, $3) ON CONFLICT (user_id, type) DO UPDATE SET token=$1, expire=now() + interval '30 minutes' RETURNING *`
return db.one(sql, [token, userID])
}
function validatePasswordResetToken (token) {
const sql = `SELECT user_id, now() < expire AS success FROM auth_tokens
WHERE token=$1 AND type='reset_password'`
return db.one(sql, [token])
.then(res => ({ userID: res.user_id, success: res.success }))
return db.one(sql, [token, type, userID])
}
function updatePassword (token, id, password) {
return validatePasswordResetToken(token).then(res => {
return validateAuthToken(token, 'reset_password').then(res => {
if (!res.success) throw new Error('Failed to verify password reset token')
return bcrypt.hash(password, 12).then(function (hash) {
return db.tx(t => {
@ -121,13 +113,6 @@ function updatePassword (token, id, password) {
})
}
function createResetPasswordToken (userID) {
const token = crypto.randomBytes(32).toString('hex')
const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, 'reset_password', $2) ON CONFLICT (user_id, type) DO UPDATE SET token=$1, expire=now() + interval '30 minutes' RETURNING *`
return db.one(sql, [token, userID])
}
function createUserRegistrationToken (username, role) {
const token = crypto.randomBytes(32).toString('hex')
const sql = `INSERT INTO user_register_tokens (token, username, role) VALUES ($1, $2, $3) ON CONFLICT (username)
@ -162,11 +147,8 @@ function changeUserRole (id, newRole) {
}
function enableUser (id) {
return db.tx(t => {
const q1 = t.none(`UPDATE users SET enabled=true WHERE id=$1`, [id])
const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id])
return t.batch([q1, q2])
})
const sql = `UPDATE users SET enabled=true WHERE id=$1`
return db.none(sql, [id])
}
function disableUser (id) {
@ -187,10 +169,8 @@ module.exports = {
updatePassword,
save2FASecret,
reset2FASecret,
validate2FAResetToken,
createReset2FAToken,
validatePasswordResetToken,
createResetPasswordToken,
validateAuthToken,
createAuthToken,
createUserRegistrationToken,
validateUserRegistrationToken,
register,

View file

@ -3,9 +3,12 @@ import classnames from 'classnames'
import React from 'react'
import OtpInput from 'react-otp-input'
import typographyStyles from 'src/components/typography/styles'
import styles from './CodeInput.styles'
const useStyles = makeStyles(styles)
const useTypographyStyles = makeStyles(typographyStyles)
const CodeInput = ({
name,
@ -18,6 +21,7 @@ const CodeInput = ({
...props
}) => {
const classes = useStyles()
const typographyClasses = useTypographyStyles()
return (
<OtpInput
@ -27,7 +31,11 @@ const CodeInput = ({
numInputs={numInputs}
separator={<span> </span>}
containerStyle={classnames(containerStyle, classes.container)}
inputStyle={classnames(inputStyle, classes.input)}
inputStyle={classnames(
inputStyle,
classes.input,
typographyClasses.confirmationCode
)}
focusStyle={classes.focus}
errorStyle={classes.error}
hasErrored={error}

View file

@ -1,17 +1,9 @@
import {
fontPrimary,
primaryColor,
zircon,
errorColor
} from 'src/styling/variables'
import { primaryColor, zircon, errorColor } from 'src/styling/variables'
const styles = {
input: {
width: '3.5rem !important',
height: '5rem',
fontFamily: fontPrimary,
fontSize: 35,
color: primaryColor,
border: '2px solid',
borderColor: zircon,
borderRadius: '4px'

View file

@ -36,15 +36,7 @@ const Subheader = ({ item, classes, user }) => {
<nav>
<ul className={classes.subheaderUl}>
{item.children.map((it, idx) => {
if (
!R.includes(
user.role,
it.allowedRoles.map(v => {
return v.key
})
)
)
return <></>
if (!R.includes(user.role, it.allowedRoles)) return <></>
return (
<li key={idx} className={classes.subheaderLi}>
<NavLink
@ -131,15 +123,7 @@ const Header = memo(({ tree, user }) => {
<nav className={classes.nav}>
<ul className={classes.ul}>
{tree.map((it, idx) => {
if (
!R.includes(
user.role,
it.allowedRoles.map(v => {
return v.key
})
)
)
return <></>
if (!R.includes(user.role, it.allowedRoles)) return <></>
return (
<NavLink
key={idx}

View file

@ -7,7 +7,8 @@ import {
fontSize5,
fontPrimary,
fontSecondary,
fontMonospaced
fontMonospaced,
fontSize2FA
} from 'src/styling/variables'
const base = {
@ -125,6 +126,12 @@ export default {
fontWeight: 500,
color: fontColor
},
confirmationCode: {
extend: base,
fontSize: fontSize2FA,
fontFamily: fontPrimary,
fontWeight: 900
},
inline: {
display: 'inline'
},

View file

@ -1,6 +1,7 @@
import { useQuery } from '@apollo/react-hooks'
import { useLazyQuery } from '@apollo/react-hooks'
import CssBaseline from '@material-ui/core/CssBaseline'
import Grid from '@material-ui/core/Grid'
import Slide from '@material-ui/core/Slide'
import {
StylesProvider,
jssPreset,
@ -10,8 +11,7 @@ import {
import gql from 'graphql-tag'
import { create } from 'jss'
import extendJss from 'jss-plugin-extend'
import * as R from 'ramda'
import React, { useContext, useState, useEffect } from 'react'
import React, { useContext, useEffect, useState } from 'react'
import {
useLocation,
useHistory,
@ -71,13 +71,7 @@ const useStyles = makeStyles({
}
})
const Main = () => {
const classes = useStyles()
const location = useLocation()
const history = useHistory()
const { wizardTested, userData, setUserData } = useContext(AppContext)
const GET_USER_DATA = gql`
const GET_USER_DATA = gql`
query userData {
userData {
id
@ -89,14 +83,24 @@ const Main = () => {
last_accessed_address
}
}
`
`
const { data: userResponse, loading } = useQuery(GET_USER_DATA)
const Main = () => {
const classes = useStyles()
const location = useLocation()
const history = useHistory()
const { wizardTested, userData, setUserData } = useContext(AppContext)
const [getUserData, { loading }] = useLazyQuery(GET_USER_DATA, {
onCompleted: userResponse => {
if (!userData && userResponse?.userData)
setUserData(userResponse.userData)
}
})
useEffect(() => {
if (!R.equals(userData, userResponse?.userData) && !loading)
setUserData(userResponse?.userData)
}, [loading, setUserData, userData, userResponse])
getUserData()
}, [getUserData])
const route = location.pathname
@ -120,7 +124,17 @@ const Main = () => {
)}
<main className={classes.wrapper}>
{sidebar && !is404 && wizardTested && (
<Slide
direction="left"
in={true}
mountOnEnter
unmountOnExit
children={
<div>
<TitleSection title={parent.title}></TitleSection>
</div>
}
/>
)}
<Grid container className={classes.grid}>
@ -143,9 +157,15 @@ const App = () => {
const [wizardTested, setWizardTested] = useState(false)
const [userData, setUserData] = useState(null)
const setRole = role => {
if (userData && userData.role !== role) {
setUserData({ ...userData, role })
}
}
return (
<AppContext.Provider
value={{ wizardTested, setWizardTested, userData, setUserData }}>
value={{ wizardTested, setWizardTested, userData, setUserData, setRole }}>
<Router>
<ApolloProvider>
<StylesProvider jss={jss}>

View file

@ -39,13 +39,7 @@ const GET_USER_DATA = gql`
}
`
const Input2FAState = ({
twoFAField,
onTwoFAChange,
clientField,
passwordField,
rememberMeField
}) => {
const Input2FAState = ({ state, dispatch }) => {
const classes = useStyles()
const history = useHistory()
const { setUserData } = useContext(AppContext)
@ -53,10 +47,26 @@ const Input2FAState = ({
const [invalidToken, setInvalidToken] = useState(false)
const handle2FAChange = value => {
onTwoFAChange(value)
dispatch({ type: 'twoFAField', payload: value })
setInvalidToken(false)
}
const handleSubmit = () => {
if (state.twoFAField.length !== 6) {
setInvalidToken(true)
return
}
input2FA({
variables: {
username: state.clientField,
password: state.passwordField,
code: state.twoFAField,
rememberMe: state.rememberMeField
}
})
}
const [input2FA, { error: mutationError }] = useMutation(INPUT_2FA, {
onCompleted: ({ input2FA: success }) => {
success ? getUserData() : setInvalidToken(true)
@ -72,13 +82,15 @@ const Input2FAState = ({
const getErrorMsg = () => {
if (queryError) return 'Internal server error'
if (twoFAField.length !== 6 && invalidToken)
if (state.twoFAField.length !== 6 && invalidToken)
return 'The code should have 6 characters!'
if (mutationError || invalidToken)
return 'Code is invalid. Please try again.'
return null
}
const errorMessage = getErrorMsg()
return (
<>
<H2 className={classes.info}>
@ -86,32 +98,15 @@ const Input2FAState = ({
</H2>
<CodeInput
name="2fa"
value={twoFAField}
value={state.twoFAField}
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
<div className={classes.twofaFooter}>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<Button
onClick={() => {
if (twoFAField.length !== 6) {
setInvalidToken(true)
return
}
input2FA({
variables: {
username: clientField,
password: passwordField,
code: twoFAField,
rememberMe: rememberMeField
}
})
}}
buttonClassName={classes.loginButton}>
{errorMessage && <P className={classes.errorMessage}>{errorMessage}</P>}
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
Login
</Button>
</div>

View file

@ -1,6 +1,6 @@
import Paper from '@material-ui/core/Paper'
import { makeStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import React, { useReducer } from 'react'
import { H2 } from 'src/components/typography'
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
@ -9,75 +9,36 @@ import Input2FAState from './Input2FAState'
import styles from './Login.styles'
import LoginState from './LoginState'
import Setup2FAState from './Setup2FAState'
import { STATES } from './states'
const useStyles = makeStyles(styles)
const STATES = {
LOGIN: 'Login',
SETUP_2FA: 'Setup 2FA',
INPUT_2FA: 'Input 2FA'
}
const LoginCard = () => {
const classes = useStyles()
const [twoFAField, setTwoFAField] = useState('')
const [clientField, setClientField] = useState('')
const [passwordField, setPasswordField] = useState('')
const [rememberMeField, setRememberMeField] = useState(false)
const [loginState, setLoginState] = useState(STATES.LOGIN)
const onClientChange = newValue => {
setClientField(newValue)
const initialState = {
twoFAField: '',
clientField: '',
passwordField: '',
rememberMeField: false,
loginState: STATES.LOGIN
}
const onPasswordChange = newValue => {
setPasswordField(newValue)
const reducer = (state, action) => {
const { type, payload } = action
return { ...state, ...payload, loginState: type }
}
const onRememberMeChange = newValue => {
setRememberMeField(newValue)
}
const onTwoFAChange = newValue => {
setTwoFAField(newValue)
}
const handleLoginState = newState => {
setLoginState(newState)
}
const [state, dispatch] = useReducer(reducer, initialState)
const renderState = () => {
switch (loginState) {
switch (state.loginState) {
case STATES.LOGIN:
return (
<LoginState
onClientChange={onClientChange}
onPasswordChange={onPasswordChange}
onRememberMeChange={onRememberMeChange}
STATES={STATES}
handleLoginState={handleLoginState}
/>
)
return <LoginState state={state} dispatch={dispatch} />
case STATES.INPUT_2FA:
return (
<Input2FAState
twoFAField={twoFAField}
onTwoFAChange={onTwoFAChange}
clientField={clientField}
passwordField={passwordField}
rememberMeField={rememberMeField}
/>
)
return <Input2FAState state={state} dispatch={dispatch} />
case STATES.SETUP_2FA:
return (
<Setup2FAState
clientField={clientField}
passwordField={passwordField}
STATES={STATES}
handleLoginState={handleLoginState}
/>
)
return <Setup2FAState state={state} dispatch={dispatch} />
default:
break
}

View file

@ -10,6 +10,7 @@ import { Checkbox, SecretInput, TextInput } from 'src/components/inputs/formik'
import { Label2, P } from 'src/components/typography'
import styles from './Login.styles'
import { STATES } from './states'
const useStyles = makeStyles(styles)
@ -33,21 +34,10 @@ const initialValues = {
rememberMe: false
}
const LoginState = ({
onClientChange,
onPasswordChange,
onRememberMeChange,
STATES,
handleLoginState
}) => {
const LoginState = ({ state, dispatch }) => {
const classes = useStyles()
const [login, { error: mutationError }] = useMutation(LOGIN, {
onCompleted: ({ login }) => {
if (login === 'INPUT2FA') handleLoginState(STATES.INPUT_2FA)
if (login === 'SETUP2FA') handleLoginState(STATES.SETUP_2FA)
}
})
const [login, { error: mutationError }] = useMutation(LOGIN)
const getErrorMsg = (formikErrors, formikTouched) => {
if (!formikErrors || !formikTouched) return null
@ -58,21 +48,36 @@ const LoginState = ({
return null
}
const submitLogin = async (username, password, rememberMe) => {
const { data: loginResponse } = await login({
variables: {
username,
password
}
})
if (!loginResponse.login) return
const stateVar =
loginResponse.login === 'INPUT2FA' ? STATES.INPUT_2FA : STATES.SETUP_2FA
return dispatch({
type: stateVar,
payload: {
clientField: username,
passwordField: password,
rememberMeField: rememberMe
}
})
}
return (
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={values => {
onClientChange(values.client)
onPasswordChange(values.password)
onRememberMeChange(values.rememberMe)
login({
variables: {
username: values.client,
password: values.password
}
})
}}>
onSubmit={values =>
submitLogin(values.client, values.password, values.rememberMe)
}>
{({ errors, touched }) => (
<Form id="login-form">
<Field

View file

@ -3,7 +3,7 @@ import { makeStyles, Grid } from '@material-ui/core'
import Paper from '@material-ui/core/Paper'
import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import React, { useReducer } from 'react'
import { useLocation, useHistory } from 'react-router-dom'
import * as Yup from 'yup'
@ -65,26 +65,33 @@ const Register = () => {
const classes = useStyles()
const history = useHistory()
const token = QueryParams().get('t')
const [username, setUsername] = useState(null)
const [role, setRole] = useState(null)
const [isLoading, setLoading] = useState(true)
const [wasSuccessful, setSuccess] = useState(false)
const { error: queryError } = useQuery(VALIDATE_REGISTER_LINK, {
const initialState = {
username: null,
role: null,
wasSuccessful: false
}
const reducer = (state, action) => {
const { type, payload } = action
return { ...state, [type]: payload }
}
const [state, dispatch] = useReducer(reducer, initialState)
const { error: queryError, loading } = useQuery(VALIDATE_REGISTER_LINK, {
variables: { token: token },
onCompleted: ({ validateRegisterLink: info }) => {
setLoading(false)
if (!info) {
setSuccess(false)
dispatch({ type: 'wasSuccessful', payload: false })
} else {
setSuccess(true)
setUsername(info.username)
setRole(info.role)
dispatch({ type: 'wasSuccessful', payload: true })
dispatch({ type: 'username', payload: info.username })
dispatch({ type: 'role', payload: info.role })
}
},
onError: () => {
setLoading(false)
setSuccess(false)
dispatch({ type: 'wasSuccessful', payload: false })
}
})
@ -120,7 +127,7 @@ const Register = () => {
<Logo className={classes.icon} />
<H2 className={classes.title}>Lamassu Admin</H2>
</div>
{!isLoading && wasSuccessful && (
{!loading && state.wasSuccessful && (
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
@ -128,9 +135,9 @@ const Register = () => {
register({
variables: {
token: token,
username: username,
username: state.username,
password: values.password,
role: role
role: state.role
}
})
}}>
@ -169,7 +176,7 @@ const Register = () => {
)}
</Formik>
)}
{!isLoading && !wasSuccessful && (
{!loading && !state.wasSuccessful && (
<>
<Label2 className={classes.inputLabel}>
Link has expired

View file

@ -1,9 +1,11 @@
import { useMutation, useQuery } from '@apollo/react-hooks'
import { useMutation, useQuery, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag'
import QRCode from 'qrcode.react'
import React, { useState } from 'react'
import React, { useContext, useState } from 'react'
import { useHistory } from 'react-router-dom'
import AppContext from 'src/AppContext'
import { ActionButton, Button } from 'src/components/buttons'
import { CodeInput } from 'src/components/inputs/base'
import { Label2, P } from 'src/components/typography'
@ -15,12 +17,14 @@ const SETUP_2FA = gql`
mutation setup2FA(
$username: String!
$password: String!
$rememberMe: Boolean!
$secret: String!
$codeConfirmation: String!
) {
setup2FA(
username: $username
password: $password
rememberMe: $rememberMe
secret: $secret
codeConfirmation: $codeConfirmation
)
@ -36,15 +40,22 @@ const GET_2FA_SECRET = gql`
}
`
const GET_USER_DATA = gql`
{
userData {
id
username
role
}
}
`
const useStyles = makeStyles(styles)
const Setup2FAState = ({
clientField,
passwordField,
STATES,
handleLoginState
}) => {
const Setup2FAState = ({ state, dispatch }) => {
const classes = useStyles()
const history = useHistory()
const { setUserData } = useContext(AppContext)
const [secret, setSecret] = useState(null)
const [otpauth, setOtpauth] = useState(null)
@ -59,21 +70,28 @@ const Setup2FAState = ({
}
const { error: queryError } = useQuery(GET_2FA_SECRET, {
variables: { username: clientField, password: passwordField },
variables: { username: state.clientField, password: state.passwordField },
onCompleted: ({ get2FASecret }) => {
setSecret(get2FASecret.secret)
setOtpauth(get2FASecret.otpauth)
}
})
const [getUserData] = useLazyQuery(GET_USER_DATA, {
onCompleted: ({ userData }) => {
setUserData(userData)
history.push('/')
}
})
const [setup2FA, { error: mutationError }] = useMutation(SETUP_2FA, {
onCompleted: ({ setup2FA: success }) => {
success ? handleLoginState(STATES.LOGIN) : setInvalidToken(true)
success ? getUserData() : setInvalidToken(true)
}
})
const getErrorMsg = () => {
if (mutationError || queryError) return 'Internal server error'
if (mutationError || queryError) return 'Internal server error.'
if (twoFAConfirmation.length !== 6 && invalidToken)
return 'The code should have 6 characters!'
if (invalidToken) return 'Code is invalid. Please try again.'
@ -135,8 +153,9 @@ const Setup2FAState = ({
}
setup2FA({
variables: {
username: clientField,
password: passwordField,
username: state.clientField,
password: state.passwordField,
rememberMe: state.rememberMeField,
secret: secret,
codeConfirmation: twoFAConfirmation
}

View file

@ -0,0 +1,7 @@
const STATES = {
LOGIN: 'LOGIN',
SETUP_2FA: 'SETUP2FA',
INPUT_2FA: 'INPUT2FA'
}
export { STATES }

View file

@ -53,10 +53,9 @@ const SessionManagement = () => {
textAlign: 'center',
size: 'sm',
view: s => {
if (R.isNil(s.sess.ua)) return 'No Record'
const ua = parser(s.sess.ua)
return s.sess.ua
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
: `No Record`
return `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
}
},
{

View file

@ -12,7 +12,6 @@ import {
} from 'react-router-dom'
import AppContext from 'src/AppContext'
// import AuthRegister from 'src/pages/AuthRegister'
import Login from 'src/pages/Authentication/Login'
import Register from 'src/pages/Authentication/Register'
import Reset2FA from 'src/pages/Authentication/Reset2FA'
@ -37,7 +36,6 @@ import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
import ServerLogs from 'src/pages/ServerLogs'
import Services from 'src/pages/Services/Services'
// import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
import SessionManagement from 'src/pages/SessionManagement/SessionManagement'
import Transactions from 'src/pages/Transactions/Transactions'
import Triggers from 'src/pages/Triggers'
@ -283,22 +281,6 @@ const tree = [
}
]
}
// {
// key: 'system',
// label: 'System',
// route: '/system',
// get component() {
// return () => <Redirect to={this.children[0].route} />
// },
// children: [
// {
// key: 'token-management',
// label: 'Token Management',
// route: '/system/token-management',
// component: TokenManagement
// }
// ]
// }
]
const map = R.map(R.when(R.has('children'), R.prop('children')))
@ -356,9 +338,7 @@ const Routes = () => {
if (!userData) return []
return flattened.filter(value => {
const keys = value.allowedRoles.map(v => {
return v.key
})
const keys = value.allowedRoles
return R.includes(userData.role, keys)
})
}
@ -379,7 +359,7 @@ const Routes = () => {
return (
<Switch>
<PrivateRoute exact path="/">
<Redirect to={{ pathname: '/transactions' }} />
<Redirect to={{ pathname: '/dashboard' }} />
</PrivateRoute>
<PrivateRoute path={'/dashboard'}>
<Transition

View file

@ -1,6 +1,11 @@
export const isLoggedIn = userData => !!userData
import * as R from 'ramda'
export const isLoggedIn = userData =>
!R.isNil(userData?.id) &&
!R.isNil(userData?.username) &&
!R.isNil(userData?.role)
export const ROLES = {
USER: { key: 'user' },
SUPERUSER: { key: 'superuser' }
USER: 'user',
SUPERUSER: 'superuser'
}

View file

@ -68,6 +68,7 @@ let fontSize2 = 20
let fontSize3 = 16
let fontSize4 = 14
let fontSize5 = 13
const fontSize2FA = 35
if (version === 8) {
fontSize1 = 32
@ -158,6 +159,7 @@ export {
fontPrimary,
fontSecondary,
fontMonospaced,
fontSize2FA,
// named font sizes
smallestFontSize,
inputFontSize,

View file

@ -12,7 +12,7 @@ import AppContext from 'src/AppContext'
const URI =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const getClient = (history, location, setUserData) =>
const getClient = (history, location, setUserData, setRole) =>
new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
@ -28,6 +28,21 @@ const getClient = (history, location, setUserData) =>
})
if (networkError) console.log(`[Network error]: ${networkError}`)
}),
new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
const context = operation.getContext()
const {
response: { headers }
} = context
if (headers) {
const role = headers.get('role')
setRole(role)
}
return response
})
}),
new HttpLink({
credentials: 'include',
uri: `${URI}/graphql`
@ -52,8 +67,8 @@ const getClient = (history, location, setUserData) =>
const Provider = ({ children }) => {
const history = useHistory()
const location = useLocation()
const { setUserData } = useContext(AppContext)
const client = getClient(history, location, setUserData)
const { setUserData, setRole } = useContext(AppContext)
const client = getClient(history, location, setUserData, setRole)
return <ApolloProvider client={client}>{children}</ApolloProvider>
}