fix: multiple small fixes across auth
This commit is contained in:
parent
9fa97725ec
commit
bbc37c0202
22 changed files with 296 additions and 291 deletions
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
44
lib/users.js
44
lib/users.js
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
new-lamassu-admin/src/pages/Authentication/states.js
Normal file
7
new-lamassu-admin/src/pages/Authentication/states.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
const STATES = {
|
||||||
|
LOGIN: 'LOGIN',
|
||||||
|
SETUP_2FA: 'SETUP2FA',
|
||||||
|
INPUT_2FA: 'INPUT2FA'
|
||||||
|
}
|
||||||
|
|
||||||
|
export { STATES }
|
||||||
|
|
@ -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`
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue