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

View file

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

View file

@ -7,9 +7,9 @@ const resolver = {
users: () => users.getUsers(), users: () => users.getUsers(),
sessions: () => sessionManager.getSessions(), sessions: () => sessionManager.getSessions(),
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username), 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), 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), validateRegisterLink: (...[, { token }]) => authentication.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) => authentication.validateResetPasswordLink(token), validateResetPasswordLink: (...[, { token }]) => authentication.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token) validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token)
@ -17,18 +17,18 @@ const resolver = {
Mutation: { Mutation: {
enableUser: (...[, { id }]) => users.enableUser(id), enableUser: (...[, { id }]) => users.enableUser(id),
disableUser: (...[, { id }]) => users.disableUser(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), deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole), changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole),
login: (...[, { username, password }]) => authentication.login(username, password), login: (...[, { username, password }]) => authentication.login(username, password),
input2FA: (root, args, context, info) => authentication.input2FA(args.username, args.password, args.rememberMe, args.code, context), input2FA: (...[, { username, password, rememberMe, code }, context]) => authentication.input2FA(username, password, rememberMe, code, context),
setup2FA: (...[, { username, password, secret, codeConfirmation }]) => authentication.setup2FA(username, password, secret, codeConfirmation), setup2FA: (...[, { username, password, rememberMe, secret, codeConfirmation }, context]) => authentication.setup2FA(username, password, rememberMe, secret, codeConfirmation, context),
createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID), createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID),
createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID), createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID),
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role), createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) => authentication.register(token, username, password, 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), resetPassword: (...[, { token, userID, newPassword }, context]) => authentication.resetPassword(token, userID, newPassword, context),
reset2FA: (root, args, context, info) => authentication.reset2FA(args.token, args.userID, args.code, args.secret, 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]) toggleUserEnable(id: ID!): User @auth(requires: [SUPERUSER])
login(username: String!, password: String!): String login(username: String!, password: String!): String
input2FA(username: String!, password: String!, code: String!, rememberMe: Boolean!): Boolean 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]) createResetPasswordToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER]) createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createRegisterToken(username: String!, role: String!): RegistrationToken @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 sql = 'SELECT id, username FROM users WHERE username=$1 AND password=$2'
const sqlUpdateLastAccessed = 'UPDATE users SET last_accessed = now() WHERE username=$1' 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 => { .then(user => {
return db.none(sqlUpdateLastAccessed, [user.username]) return db.none(sqlUpdateLastAccessed, [user.username])
.then(() => user) .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 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 })) .then(res => ({ userID: res.user_id, success: res.success }))
} }
function reset2FASecret (token, id, secret) { 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') if (!res.success) throw new Error('Failed to verify 2FA reset token')
return db.tx(t => { return db.tx(t => {
const q1 = t.none('UPDATE users SET twofa_code=$1 WHERE id=$2', [secret, id]) 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 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]) return db.one(sql, [token, type, 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 }))
} }
function updatePassword (token, id, password) { 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') if (!res.success) throw new Error('Failed to verify password reset token')
return bcrypt.hash(password, 12).then(function (hash) { return bcrypt.hash(password, 12).then(function (hash) {
return db.tx(t => { 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) { function createUserRegistrationToken (username, role) {
const token = crypto.randomBytes(32).toString('hex') const token = crypto.randomBytes(32).toString('hex')
const sql = `INSERT INTO user_register_tokens (token, username, role) VALUES ($1, $2, $3) ON CONFLICT (username) 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) { function enableUser (id) {
return db.tx(t => { const sql = `UPDATE users SET enabled=true WHERE id=$1`
const q1 = t.none(`UPDATE users SET enabled=true WHERE id=$1`, [id]) return db.none(sql, [id])
const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id])
return t.batch([q1, q2])
})
} }
function disableUser (id) { function disableUser (id) {
@ -187,10 +169,8 @@ module.exports = {
updatePassword, updatePassword,
save2FASecret, save2FASecret,
reset2FASecret, reset2FASecret,
validate2FAResetToken, validateAuthToken,
createReset2FAToken, createAuthToken,
validatePasswordResetToken,
createResetPasswordToken,
createUserRegistrationToken, createUserRegistrationToken,
validateUserRegistrationToken, validateUserRegistrationToken,
register, register,

View file

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

View file

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

View file

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

View file

@ -7,7 +7,8 @@ import {
fontSize5, fontSize5,
fontPrimary, fontPrimary,
fontSecondary, fontSecondary,
fontMonospaced fontMonospaced,
fontSize2FA
} from 'src/styling/variables' } from 'src/styling/variables'
const base = { const base = {
@ -125,6 +126,12 @@ export default {
fontWeight: 500, fontWeight: 500,
color: fontColor color: fontColor
}, },
confirmationCode: {
extend: base,
fontSize: fontSize2FA,
fontFamily: fontPrimary,
fontWeight: 900
},
inline: { inline: {
display: '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 CssBaseline from '@material-ui/core/CssBaseline'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import Slide from '@material-ui/core/Slide'
import { import {
StylesProvider, StylesProvider,
jssPreset, jssPreset,
@ -10,8 +11,7 @@ import {
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { create } from 'jss' import { create } from 'jss'
import extendJss from 'jss-plugin-extend' import extendJss from 'jss-plugin-extend'
import * as R from 'ramda' import React, { useContext, useEffect, useState } from 'react'
import React, { useContext, useState, useEffect } from 'react'
import { import {
useLocation, useLocation,
useHistory, useHistory,
@ -71,12 +71,6 @@ 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 { query userData {
userData { userData {
@ -91,12 +85,22 @@ const Main = () => {
} }
` `
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(() => { useEffect(() => {
if (!R.equals(userData, userResponse?.userData) && !loading) getUserData()
setUserData(userResponse?.userData) }, [getUserData])
}, [loading, setUserData, userData, userResponse])
const route = location.pathname const route = location.pathname
@ -120,7 +124,17 @@ const Main = () => {
)} )}
<main className={classes.wrapper}> <main className={classes.wrapper}>
{sidebar && !is404 && wizardTested && ( {sidebar && !is404 && wizardTested && (
<Slide
direction="left"
in={true}
mountOnEnter
unmountOnExit
children={
<div>
<TitleSection title={parent.title}></TitleSection> <TitleSection title={parent.title}></TitleSection>
</div>
}
/>
)} )}
<Grid container className={classes.grid}> <Grid container className={classes.grid}>
@ -143,9 +157,15 @@ const App = () => {
const [wizardTested, setWizardTested] = useState(false) const [wizardTested, setWizardTested] = useState(false)
const [userData, setUserData] = useState(null) const [userData, setUserData] = useState(null)
const setRole = role => {
if (userData && userData.role !== role) {
setUserData({ ...userData, role })
}
}
return ( return (
<AppContext.Provider <AppContext.Provider
value={{ wizardTested, setWizardTested, userData, setUserData }}> value={{ wizardTested, setWizardTested, userData, setUserData, setRole }}>
<Router> <Router>
<ApolloProvider> <ApolloProvider>
<StylesProvider jss={jss}> <StylesProvider jss={jss}>

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import { makeStyles, Grid } from '@material-ui/core'
import Paper from '@material-ui/core/Paper' import Paper from '@material-ui/core/Paper'
import { Field, Form, Formik } from 'formik' import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import React, { useState } from 'react' import React, { useReducer } from 'react'
import { useLocation, useHistory } from 'react-router-dom' import { useLocation, useHistory } from 'react-router-dom'
import * as Yup from 'yup' import * as Yup from 'yup'
@ -65,26 +65,33 @@ const Register = () => {
const classes = useStyles() const classes = useStyles()
const history = useHistory() const history = useHistory()
const token = QueryParams().get('t') 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 }, variables: { token: token },
onCompleted: ({ validateRegisterLink: info }) => { onCompleted: ({ validateRegisterLink: info }) => {
setLoading(false)
if (!info) { if (!info) {
setSuccess(false) dispatch({ type: 'wasSuccessful', payload: false })
} else { } else {
setSuccess(true) dispatch({ type: 'wasSuccessful', payload: true })
setUsername(info.username) dispatch({ type: 'username', payload: info.username })
setRole(info.role) dispatch({ type: 'role', payload: info.role })
} }
}, },
onError: () => { onError: () => {
setLoading(false) dispatch({ type: 'wasSuccessful', payload: false })
setSuccess(false)
} }
}) })
@ -120,7 +127,7 @@ const Register = () => {
<Logo className={classes.icon} /> <Logo className={classes.icon} />
<H2 className={classes.title}>Lamassu Admin</H2> <H2 className={classes.title}>Lamassu Admin</H2>
</div> </div>
{!isLoading && wasSuccessful && ( {!loading && state.wasSuccessful && (
<Formik <Formik
validationSchema={validationSchema} validationSchema={validationSchema}
initialValues={initialValues} initialValues={initialValues}
@ -128,9 +135,9 @@ const Register = () => {
register({ register({
variables: { variables: {
token: token, token: token,
username: username, username: state.username,
password: values.password, password: values.password,
role: role role: state.role
} }
}) })
}}> }}>
@ -169,7 +176,7 @@ const Register = () => {
)} )}
</Formik> </Formik>
)} )}
{!isLoading && !wasSuccessful && ( {!loading && !state.wasSuccessful && (
<> <>
<Label2 className={classes.inputLabel}> <Label2 className={classes.inputLabel}>
Link has expired 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 { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import QRCode from 'qrcode.react' 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 { ActionButton, Button } from 'src/components/buttons'
import { CodeInput } from 'src/components/inputs/base' import { CodeInput } from 'src/components/inputs/base'
import { Label2, P } from 'src/components/typography' import { Label2, P } from 'src/components/typography'
@ -15,12 +17,14 @@ const SETUP_2FA = gql`
mutation setup2FA( mutation setup2FA(
$username: String! $username: String!
$password: String! $password: String!
$rememberMe: Boolean!
$secret: String! $secret: String!
$codeConfirmation: String! $codeConfirmation: String!
) { ) {
setup2FA( setup2FA(
username: $username username: $username
password: $password password: $password
rememberMe: $rememberMe
secret: $secret secret: $secret
codeConfirmation: $codeConfirmation 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 useStyles = makeStyles(styles)
const Setup2FAState = ({ const Setup2FAState = ({ state, dispatch }) => {
clientField,
passwordField,
STATES,
handleLoginState
}) => {
const classes = useStyles() const classes = useStyles()
const history = useHistory()
const { setUserData } = useContext(AppContext)
const [secret, setSecret] = useState(null) const [secret, setSecret] = useState(null)
const [otpauth, setOtpauth] = useState(null) const [otpauth, setOtpauth] = useState(null)
@ -59,21 +70,28 @@ const Setup2FAState = ({
} }
const { error: queryError } = useQuery(GET_2FA_SECRET, { const { error: queryError } = useQuery(GET_2FA_SECRET, {
variables: { username: clientField, password: passwordField }, variables: { username: state.clientField, password: state.passwordField },
onCompleted: ({ get2FASecret }) => { onCompleted: ({ get2FASecret }) => {
setSecret(get2FASecret.secret) setSecret(get2FASecret.secret)
setOtpauth(get2FASecret.otpauth) setOtpauth(get2FASecret.otpauth)
} }
}) })
const [getUserData] = useLazyQuery(GET_USER_DATA, {
onCompleted: ({ userData }) => {
setUserData(userData)
history.push('/')
}
})
const [setup2FA, { error: mutationError }] = useMutation(SETUP_2FA, { const [setup2FA, { error: mutationError }] = useMutation(SETUP_2FA, {
onCompleted: ({ setup2FA: success }) => { onCompleted: ({ setup2FA: success }) => {
success ? handleLoginState(STATES.LOGIN) : setInvalidToken(true) success ? getUserData() : setInvalidToken(true)
} }
}) })
const getErrorMsg = () => { const getErrorMsg = () => {
if (mutationError || queryError) return 'Internal server error' if (mutationError || queryError) return 'Internal server error.'
if (twoFAConfirmation.length !== 6 && invalidToken) if (twoFAConfirmation.length !== 6 && invalidToken)
return 'The code should have 6 characters!' return 'The code should have 6 characters!'
if (invalidToken) return 'Code is invalid. Please try again.' if (invalidToken) return 'Code is invalid. Please try again.'
@ -135,8 +153,9 @@ const Setup2FAState = ({
} }
setup2FA({ setup2FA({
variables: { variables: {
username: clientField, username: state.clientField,
password: passwordField, password: state.passwordField,
rememberMe: state.rememberMeField,
secret: secret, secret: secret,
codeConfirmation: twoFAConfirmation 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', textAlign: 'center',
size: 'sm', size: 'sm',
view: s => { view: s => {
if (R.isNil(s.sess.ua)) return 'No Record'
const ua = parser(s.sess.ua) const ua = parser(s.sess.ua)
return s.sess.ua return `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
: `No Record`
} }
}, },
{ {

View file

@ -12,7 +12,6 @@ import {
} from 'react-router-dom' } from 'react-router-dom'
import AppContext from 'src/AppContext' import AppContext from 'src/AppContext'
// import AuthRegister from 'src/pages/AuthRegister'
import Login from 'src/pages/Authentication/Login' import Login from 'src/pages/Authentication/Login'
import Register from 'src/pages/Authentication/Register' import Register from 'src/pages/Authentication/Register'
import Reset2FA from 'src/pages/Authentication/Reset2FA' 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 TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
import ServerLogs from 'src/pages/ServerLogs' import ServerLogs from 'src/pages/ServerLogs'
import Services from 'src/pages/Services/Services' import Services from 'src/pages/Services/Services'
// import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
import SessionManagement from 'src/pages/SessionManagement/SessionManagement' import SessionManagement from 'src/pages/SessionManagement/SessionManagement'
import Transactions from 'src/pages/Transactions/Transactions' import Transactions from 'src/pages/Transactions/Transactions'
import Triggers from 'src/pages/Triggers' 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'))) const map = R.map(R.when(R.has('children'), R.prop('children')))
@ -356,9 +338,7 @@ const Routes = () => {
if (!userData) return [] if (!userData) return []
return flattened.filter(value => { return flattened.filter(value => {
const keys = value.allowedRoles.map(v => { const keys = value.allowedRoles
return v.key
})
return R.includes(userData.role, keys) return R.includes(userData.role, keys)
}) })
} }
@ -379,7 +359,7 @@ const Routes = () => {
return ( return (
<Switch> <Switch>
<PrivateRoute exact path="/"> <PrivateRoute exact path="/">
<Redirect to={{ pathname: '/transactions' }} /> <Redirect to={{ pathname: '/dashboard' }} />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path={'/dashboard'}> <PrivateRoute path={'/dashboard'}>
<Transition <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 = { export const ROLES = {
USER: { key: 'user' }, USER: 'user',
SUPERUSER: { key: 'superuser' } SUPERUSER: 'superuser'
} }

View file

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

View file

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