fix: email verification and UX

fix: remove annotations

fix: styles

fix: move directives from schema

chore: rework auth routes

feat: start graphql schema modularization

feat: start directives rework

fix: directive cycle

fix: directive resolve

fix: schema auth directive

feat: migrate auth routes to gql

fix: apollo client

fix: migrate forms to formik

refactor: user resolver

chore: final touches on auth components

fix: routes
This commit is contained in:
Sérgio Salgado 2020-12-14 17:33:47 +00:00 committed by Josh Harvey
parent fded22f39a
commit d295acc261
33 changed files with 1319 additions and 1139 deletions

View file

@ -21,7 +21,9 @@ const options = require('../options')
const db = require('../db')
const users = require('../users')
const { typeDefs, resolvers, AuthDirective, SuperuserDirective } = require('./graphql/schema')
const authRouter = require('./routes/auth')
const { AuthDirective } = require('./graphql/directives')
const { typeDefs, resolvers } = require('./graphql/schema')
const devMode = require('minimist')(process.argv.slice(2)).dev
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
@ -64,8 +66,7 @@ const apolloServer = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
auth: AuthDirective,
superuser: SuperuserDirective
auth: AuthDirective
},
playground: false,
introspection: false,
@ -74,7 +75,8 @@ const apolloServer = new ApolloServer({
return error
},
context: async ({ req }) => {
if (!req.session.user) throw new AuthenticationError('Authentication failed')
if (!req.session.user) return { req }
const user = await users.verifyAndUpdateUser(
req.session.user.id,
req.headers['user-agent'] || 'Unknown',
@ -87,7 +89,8 @@ const apolloServer = new ApolloServer({
req.session.lastUsed = new Date(Date.now()).toISOString()
req.session.user.id = user.id
req.session.user.role = user.role
return { req: { ...req } }
return { req }
}
})
@ -104,9 +107,7 @@ app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' })
app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false }))
app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false }))
app.use('/api', register)
require('./routes/auth')(app)
app.use(authRouter)
// Everything not on graphql or api/register is redirected to the front-end
app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))

View file

@ -0,0 +1,40 @@
const _ = require('lodash/fp')
const { SchemaDirectiveVisitor, AuthenticationError } = require('apollo-server-express')
const { defaultFieldResolver } = require('graphql')
class AuthDirective extends SchemaDirectiveVisitor {
visitObject (type) {
this.ensureFieldsWrapped(type)
type._requiredAuthRole = this.args.requires
}
visitFieldDefinition (field, details) {
this.ensureFieldsWrapped(details.objectType)
field._requiredAuthRole = this.args.requires
}
ensureFieldsWrapped (objectType) {
if (objectType._authFieldsWrapped) return
objectType._authFieldsWrapped = true
const fields = objectType.getFields()
_.forEach(fieldName => {
const field = fields[fieldName]
const { resolve = defaultFieldResolver } = field
field.resolve = function (root, args, context, info) {
const requiredRoles = field._requiredAuthRole ? field._requiredAuthRole : objectType._requiredAuthRole
if (!requiredRoles) return resolve.apply(this, [root, args, context, info])
const user = context.req.session.user
if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) throw new AuthenticationError('You do not have permission to access this resource!')
return resolve.apply(this, [root, args, context, info])
}
}, _.keys(fields))
}
}
module.exports = AuthDirective

View file

@ -0,0 +1,3 @@
const AuthDirective = require('./auth')
module.exports = { AuthDirective }

View file

@ -0,0 +1,220 @@
const otplib = require('otplib')
const bcrypt = require('bcrypt')
const loginHelper = require('../../services/login')
const T = require('../../../time')
const users = require('../../../users')
const sessionManager = require('../../../session-manager')
const REMEMBER_ME_AGE = 90 * T.day
async function authenticateUser (username, password) {
const hashedPassword = await loginHelper.checkUser(username)
if (!hashedPassword) return null
const isMatch = await bcrypt.compare(password, hashedPassword)
if (!isMatch) return null
const user = await loginHelper.validateUser(username, hashedPassword)
if (!user) return null
return user
}
const getUserData = context => {
const lidCookie = context.req.cookies && context.req.cookies.lid
if (!lidCookie) return null
const user = context.req.session.user
return user
}
const get2FASecret = (username, password) => {
return authenticateUser(username, password).then(user => {
if (!user) return null
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(username, 'Lamassu Industries', secret)
return { secret, otpauth }
})
}
const confirm2FA = (codeArg, context) => {
const code = codeArg
const requestingUser = context.req.session.user
if (!requestingUser) return false
return users.get2FASecret(requestingUser.id).then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) return false
return true
})
}
const validateRegisterLink = token => {
if (!token) return null
return users.validateUserRegistrationToken(token)
.then(r => {
if (!r.success) return null
return { username: r.username, role: r.role }
})
.catch(err => console.error(err))
}
const validateResetPasswordLink = token => {
if (!token) return null
return users.validatePasswordResetToken(token)
.then(r => {
if (!r.success) return null
return { id: r.userID }
})
.catch(err => console.error(err))
}
const validateReset2FALink = token => {
if (!token) return null
return users.validate2FAResetToken(token)
.then(r => {
if (!r.success) return null
return users.findById(r.userID)
})
.then(user => {
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(user.username, 'Lamassu Industries', secret)
return { user_id: user.id, secret, otpauth }
})
.catch(err => console.error(err))
}
const deleteSession = (sessionID, context) => {
if (sessionID === context.req.session.id) {
context.req.session.destroy()
}
return sessionManager.deleteSession(sessionID)
}
const login = (username, password) => {
return authenticateUser(username, password).then(user => {
if (!user) return 'FAILED'
return users.get2FASecret(user.id).then(user => {
const twoFASecret = user.twofa_code
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
})
})
}
const input2FA = (username, password, rememberMe, code, context) => {
return authenticateUser(username, password).then(user => {
if (!user) return false
return users.get2FASecret(user.id).then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) return false
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
})
})
}
const setup2FA = (username, password, secret, codeConfirmation) => {
return authenticateUser(username, password).then(user => {
if (!user || !secret) return false
const isCodeValid = otplib.authenticator.verify({ token: codeConfirmation, secret: secret })
if (!isCodeValid) return false
users.save2FASecret(user.id, secret)
return true
})
}
const createResetPasswordToken = userID => {
return users.findById(userID)
.then(user => {
if (!user) return null
return users.createResetPasswordToken(user.id)
})
.then(token => {
return token
})
.catch(err => console.error(err))
}
const createReset2FAToken = userID => {
return users.findById(userID)
.then(user => {
if (!user) return null
return users.createReset2FAToken(user.id)
})
.then(token => {
return token
})
.catch(err => console.error(err))
}
const createRegisterToken = (username, role) => {
return users.getByName(username)
.then(user => {
if (user) return null
return users.createUserRegistrationToken(username, role).then(token => {
return token
})
})
.catch(err => console.error(err))
}
const register = (username, password, role) => {
return users.getByName(username)
.then(user => {
if (user) return false
users.createUser(username, password, role)
return true
})
.catch(err => console.error(err))
}
const resetPassword = (userID, newPassword, context) => {
return users.findById(userID).then(user => {
if (!user) return false
if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy()
return users.updatePassword(user.id, newPassword)
}).then(() => { return true }).catch(err => console.error(err))
}
const reset2FA = (userID, code, secret, context) => {
return users.findById(userID).then(user => {
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) return false
if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy()
return users.save2FASecret(user.id, secret).then(() => { return true })
}).catch(err => console.error(err))
}
module.exports = {
getUserData,
get2FASecret,
confirm2FA,
validateRegisterLink,
validateResetPasswordLink,
validateReset2FALink,
deleteSession,
login,
input2FA,
setup2FA,
createResetPasswordToken,
createReset2FAToken,
createRegisterToken,
register,
resetPassword,
reset2FA
}

View file

@ -16,6 +16,7 @@ const scalar = require('./scalar.resolver')
const settings = require('./settings.resolver')
const status = require('./status.resolver')
const transaction = require('./transaction.resolver')
const user = require('./users.resolver')
const version = require('./version.resolver')
const resolvers = [
@ -35,6 +36,7 @@ const resolvers = [
settings,
status,
transaction,
user,
version
]

View file

@ -0,0 +1,35 @@
const authentication = require('../modules/authentication')
const users = require('../../../users')
const sessionManager = require('../../../session-manager')
const resolver = {
Query: {
users: () => users.getUsers(),
sessions: () => sessionManager.getSessionList(),
userSessions: (...[, { username }]) => sessionManager.getUserSessions(username),
userData: (root, args, context, info) => authentication.getUserData(context),
get2FASecret: (...[, { username, password }]) => authentication.get2FASecret(username, password),
confirm2FA: (root, args, context, info) => authentication.confirm2FA(args.code, context),
validateRegisterLink: (...[, { token }]) => authentication.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) => authentication.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token)
},
Mutation: {
deleteUser: (...[, { id }]) => users.deleteUser(id),
deleteSession: (root, args, context, info) => authentication.deleteSession(args.sid, context),
deleteUserSessions: (...[, { username }]) => sessionManager.deleteUserSessions(username),
changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole),
toggleUserEnable: (...[, { id }]) => users.toggleUserEnable(id),
login: (...[, { username, password }]) => authentication.login(username, password),
input2FA: (root, args, context, info) => authentication.input2FA(args.username, args.password, args.rememberMe, args.code, context),
setup2FA: (...[, { username, password, secret, codeConfirmation }]) => authentication.setup2FA(username, password, secret, codeConfirmation),
createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID),
createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID),
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
register: (...[, { username, password, role }]) => authentication.register(username, password, role),
resetPassword: (root, args, context, info) => authentication.resetPassword(args.userID, args.newPassword, context),
reset2FA: (root, args, context, info) => authentication.reset2FA(args.userID, args.code, args.secret, context)
}
}
module.exports = resolver

View file

@ -16,6 +16,7 @@ const scalar = require('./scalar.type')
const settings = require('./settings.type')
const status = require('./status.type')
const transaction = require('./transaction.type')
const user = require('./users.type')
const version = require('./version.type')
const types = [
@ -35,6 +36,7 @@ const types = [
settings,
status,
transaction,
user,
version
]

View file

@ -0,0 +1,77 @@
const typeDef = `
directive @auth(
requires: [Role] = [USER, SUPERUSER]
) on OBJECT | FIELD_DEFINITION
enum Role {
SUPERUSER
USER
}
type UserSession {
sid: String!
sess: JSONObject!
expire: Date!
}
type User {
id: ID
username: String
role: String
enabled: Boolean
created: Date
last_accessed: Date
last_accessed_from: String
last_accessed_address: String
}
type TwoFactorSecret {
user_id: ID
secret: String!
otpauth: String!
}
type ResetToken {
token: String
user_id: ID
expire: Date
}
type RegistrationToken {
token: String
username: String
role: String
expire: Date
}
type Query {
users: [User] @auth(requires: [SUPERUSER])
sessions: [UserSession] @auth(requires: [SUPERUSER])
userSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER])
userData: User
get2FASecret(username: String!, password: String!): TwoFactorSecret
confirm2FA(code: String!): Boolean @auth(requires: [SUPERUSER])
validateRegisterLink(token: String!): User
validateResetPasswordLink(token: String!): User
validateReset2FALink(token: String!): TwoFactorSecret
}
type Mutation {
deleteUser(id: ID!): User @auth(requires: [SUPERUSER])
deleteSession(sid: String!): UserSession @auth(requires: [SUPERUSER])
deleteUserSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER])
changeUserRole(id: ID!, newRole: String!): User @auth(requires: [SUPERUSER])
toggleUserEnable(id: ID!): User @auth(requires: [SUPERUSER])
login(username: String!, password: String!): String
input2FA(username: String!, password: String!, code: String!, rememberMe: Boolean!): Boolean
setup2FA(username: String!, password: String!, secret: String!, codeConfirmation: String!): Boolean
createResetPasswordToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER])
register(username: String!, password: String!, role: String!): Boolean
resetPassword(userID: ID!, newPassword: String!): Boolean
reset2FA(userID: ID!, secret: String!, code: String!): Boolean
}
`
module.exports = typeDef

View file

@ -1,91 +1,7 @@
const otplib = require('otplib')
const bcrypt = require('bcrypt')
const express = require('express')
const router = express.Router()
const users = require('../../users')
const login = require('../login')
async function isValidUser (username, password) {
const hashedPassword = await login.checkUser(username)
if (!hashedPassword) return false
const isMatch = await bcrypt.compare(password, hashedPassword)
if (!isMatch) return false
const user = await login.validateUser(username, hashedPassword)
if (!user) return false
return user
}
module.exports = function (app) {
app.post('/api/login', function (req, res, next) {
const usernameInput = req.body.username
const passwordInput = req.body.password
isValidUser(usernameInput, passwordInput).then(user => {
if (!user) return res.sendStatus(403)
users.get2FASecret(user.id).then(user => {
const twoFASecret = user.twofa_code
if (twoFASecret) return res.status(200).json({ message: 'INPUT2FA' })
if (!twoFASecret) return res.status(200).json({ message: 'SETUP2FA' })
})
})
})
app.post('/api/login/2fa', function (req, res, next) {
const code = req.body.twoFACode
const username = req.body.username
const password = req.body.password
const rememberMeInput = req.body.rememberMe
isValidUser(username, password).then(user => {
if (!user) return res.sendStatus(403)
users.get2FASecret(user.id).then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) return res.sendStatus(403)
const finalUser = { id: user.id, username: user.username, role: user.role }
req.session.user = finalUser
if (rememberMeInput) req.session.cookie.maxAge = 90 * 24 * 60 * 60 * 1000 // 90 days
return res.sendStatus(200)
})
})
})
app.post('/api/login/2fa/setup', function (req, res, next) {
const username = req.body.username
const password = req.body.password
// TODO: maybe check if the user already has a 2fa secret
isValidUser(username, password).then(user => {
if (!user) return res.sendStatus(403)
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(username, 'Lamassu Industries', secret)
return res.status(200).json({ secret, otpauth })
})
})
app.post('/api/login/2fa/save', function (req, res, next) {
const username = req.body.username
const password = req.body.password
const secret = req.body.secret
const code = req.body.code
isValidUser(username, password).then(user => {
if (!user || !secret) return res.sendStatus(403)
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) return res.sendStatus(403)
users.save2FASecret(user.id, secret)
return res.sendStatus(200)
})
})
app.get('/user-data', function (req, res, next) {
const getUserData = function (req, res, next) {
const lidCookie = req.cookies && req.cookies.lid
if (!lidCookie) {
res.sendStatus(403)
@ -94,166 +10,8 @@ module.exports = function (app) {
const user = req.session.user
return res.status(200).json({ message: 'Success', user: user })
})
app.post('/api/resetpassword', function (req, res, next) {
const userID = req.body.userID
users.findById(userID)
.then(user => {
if (!user) return res.sendStatus(403)
return users.createResetPasswordToken(user.id)
})
.then(token => {
return res.status(200).json({ token })
})
.catch(err => console.log(err))
})
app.get('/api/resetpassword', function (req, res, next) {
const token = req.query.t
if (!token) return res.sendStatus(400)
return users.validatePasswordResetToken(token)
.then(r => {
if (!r.success) return res.status(200).send('The link has expired')
return res.status(200).json({ userID: r.userID })
})
.catch(err => {
console.log(err)
res.sendStatus(400)
})
})
app.post('/api/updatepassword', function (req, res, next) {
const userID = req.body.userID
const newPassword = req.body.newPassword
users.findById(userID).then(user => {
if (req.session.user && user.id === req.session.user.id) req.session.destroy()
return users.updatePassword(user.id, newPassword)
}).then(() => {
res.sendStatus(200)
}).catch(err => {
console.log(err)
res.sendStatus(400)
})
})
app.post('/api/reset2fa', function (req, res, next) {
const userID = req.body.userID
users.findById(userID)
.then(user => {
if (!user) return res.sendStatus(403)
return users.createReset2FAToken(user.id)
})
.then(token => {
return res.status(200).json({ token })
})
.catch(err => console.log(err))
})
app.get('/api/reset2fa', function (req, res, next) {
const token = req.query.t
if (!token) return res.sendStatus(400)
return users.validate2FAResetToken(token)
.then(r => {
if (!r.success) return res.status(200).send('The link has expired')
return users.findById(r.userID)
})
.then(user => {
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(user.username, 'Lamassu Industries', secret)
return res.status(200).json({ userID: user.id, secret, otpauth })
})
.catch(err => {
console.log(err)
res.sendStatus(400)
})
})
app.post('/api/update2fa', function (req, res, next) {
const userID = req.body.userID
const secret = req.body.secret
const code = req.body.code
users.findById(userID).then(user => {
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) return res.sendStatus(401)
if (req.session.user && user.id === req.session.user.id) req.session.destroy()
users.save2FASecret(user.id, secret).then(() => { return res.sendStatus(200) })
}).catch(err => {
console.log(err)
return res.sendStatus(400)
})
})
app.post('/api/createuser', function (req, res, next) {
const username = req.body.username
const role = req.body.role
users.getByName(username)
.then(user => {
if (user) return res.status(200).json({ message: 'User already exists!' })
users.createUserRegistrationToken(username, role).then(token => {
return res.status(200).json({ token })
})
})
.catch(err => {
console.log(err)
res.sendStatus(400)
})
})
app.get('/api/register', function (req, res, next) {
const token = req.query.t
if (!token) return res.sendStatus(400)
users.validateUserRegistrationToken(token)
.then(r => {
if (!r.success) return res.status(200).json({ message: 'The link has expired' })
return res.status(200).json({ username: r.username, role: r.role })
})
.catch(err => {
console.log(err)
res.sendStatus(400)
})
})
app.post('/api/register', function (req, res, next) {
const username = req.body.username
const password = req.body.password
const role = req.body.role
users.getByName(username)
.then(user => {
if (user) return res.status(200).json({ message: 'User already exists!' })
users.createUser(username, password, role)
res.sendStatus(200)
})
.catch(err => {
console.log(err)
res.sendStatus(400)
})
})
app.post('/api/confirm2fa', function (req, res, next) {
const code = req.body.code
const requestingUser = req.session.user
if (!requestingUser) return res.status(403)
users.get2FASecret(requestingUser.id).then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) return res.sendStatus(401)
return res.sendStatus(200)
})
})
}
router.get('/user-data', getUserData)
module.exports = router

View file

@ -9,7 +9,7 @@ import {
import { axios } from '@use-hooks/axios'
import { create } from 'jss'
import extendJss from 'jss-plugin-extend'
import React, { createContext, useContext, useEffect, useState } from 'react'
import React, { useContext, useEffect, useState } from 'react'
import {
useLocation,
useHistory,

View file

@ -1,5 +1,6 @@
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import axios from 'axios'
import gql from 'graphql-tag'
import React, { useContext, useState } from 'react'
import { useHistory } from 'react-router-dom'
@ -10,11 +11,34 @@ import { H2, P } from 'src/components/typography'
import styles from './Login.styles'
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const useStyles = makeStyles(styles)
const INPUT_2FA = gql`
mutation input2FA(
$username: String!
$password: String!
$code: String!
$rememberMe: Boolean!
) {
input2FA(
username: $username
password: $password
code: $code
rememberMe: $rememberMe
)
}
`
const GET_USER_DATA = gql`
{
userData {
id
username
role
}
}
`
const Input2FAState = ({
twoFAField,
onTwoFAChange,
@ -33,53 +57,25 @@ const Input2FAState = ({
setInvalidToken(false)
}
const handle2FA = () => {
axios({
method: 'POST',
url: `${url}/api/login/2fa`,
data: {
username: clientField,
password: passwordField,
rememberMe: rememberMeField,
twoFACode: twoFAField
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
const [input2FA, { error: mutationError }] = useMutation(INPUT_2FA, {
onCompleted: ({ input2FA: success }) => {
success ? getUserData() : setInvalidToken(true)
}
})
.then((res, err) => {
if (err) return
if (res) {
const status = res.status
if (status === 200) {
getUserData()
const [getUserData, { error: queryError }] = useLazyQuery(GET_USER_DATA, {
onCompleted: ({ userData }) => {
setUserData(userData)
history.push('/')
}
}
})
.catch(err => {
if (err.response && err.response.data) {
if (err.response.status === 403) {
onTwoFAChange('')
setInvalidToken(true)
}
}
})
}
const getUserData = () => {
axios({
method: 'GET',
url: `${url}/user-data`,
withCredentials: true
})
.then(res => {
if (res.status === 200) setUserData(res.data.user)
})
.catch(err => {
if (err.status === 403) setUserData(null)
})
const getErrorMsg = () => {
if (mutationError || queryError) return 'Internal server error'
if (twoFAField.length !== 6 && invalidToken)
return 'The code should have 6 characters!'
if (invalidToken) return 'Code is invalid. Please try again.'
return null
}
return (
@ -93,16 +89,26 @@ const Input2FAState = ({
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
<div className={classes.twofaFooter}>
{invalidToken && (
<P className={classes.errorMessage}>
Code is invalid. Please try again.
</P>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<Button
onClick={() => {
handle2FA()
if (twoFAField.length !== 6) {
setInvalidToken(true)
return
}
input2FA({
variables: {
username: clientField,
password: passwordField,
code: twoFAField,
rememberMe: rememberMeField
}
})
}}
buttonClassName={classes.loginButton}>
Login

View file

@ -17,7 +17,6 @@ const Login = () => {
direction="column"
alignItems="center"
justify="center"
style={{ minHeight: '100vh' }}
className={classes.welcomeBackground}>
<Grid>
<LoginCard />

View file

@ -35,6 +35,7 @@ const styles = {
marginBottom: 30
},
rememberMeWrapper: {
marginTop: 35,
display: 'flex',
flexDirection: 'row'
},
@ -61,15 +62,14 @@ const styles = {
background: 'url(/wizard-background.svg) no-repeat center center fixed',
backgroundColor: backgroundColor,
backgroundSize: 'cover',
// filter: 'blur(4px)',
// pointerEvents: 'none',
height: '100vh',
width: '100vw',
position: 'relative',
left: '50%',
right: '50%',
marginLeft: '-50vw',
marginRight: '-50vw'
marginRight: '-50vw',
minHeight: '100vh'
},
info: {
fontFamily: fontSecondary,
@ -115,6 +115,12 @@ const styles = {
},
confirm2FAInput: {
marginTop: 25
},
confirmPassword: {
marginTop: 25
},
error: {
color: errorColor
}
}

View file

@ -52,11 +52,8 @@ const LoginCard = () => {
case STATES.LOGIN:
return (
<LoginState
clientField={clientField}
onClientChange={onClientChange}
passwordField={passwordField}
onPasswordChange={onPasswordChange}
rememberMeField={rememberMeField}
onRememberMeChange={onRememberMeChange}
STATES={STATES}
handleLoginState={handleLoginState}

View file

@ -1,128 +1,134 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import axios from 'axios'
import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import * as Yup from 'yup'
import { Button } from 'src/components/buttons'
import { Checkbox, TextInput } from 'src/components/inputs/base'
import { Checkbox, SecretInput, TextInput } from 'src/components/inputs/formik'
import { Label2, P } from 'src/components/typography'
import styles from './Login.styles'
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const useStyles = makeStyles(styles)
const LOGIN = gql`
mutation login($username: String!, $password: String!) {
login(username: $username, password: $password)
}
`
const validationSchema = Yup.object().shape({
client: Yup.string()
.required('Client field is required!')
.email('Username field should be in an email format!'),
password: Yup.string().required('Password field is required'),
rememberMe: Yup.boolean()
})
const initialValues = {
client: '',
password: '',
rememberMe: false
}
const LoginState = ({
clientField,
onClientChange,
passwordField,
onPasswordChange,
rememberMeField,
onRememberMeChange,
STATES,
handleLoginState
}) => {
const classes = useStyles()
const [login, { error: mutationError }] = useMutation(LOGIN, {
onCompleted: ({ login }) => {
if (login === 'INPUT2FA') handleLoginState(STATES.INPUT_2FA)
if (login === 'SETUP2FA') handleLoginState(STATES.SETUP_2FA)
if (login === 'FAILED') setInvalidLogin(true)
}
})
const [invalidLogin, setInvalidLogin] = useState(false)
const handleClientChange = event => {
onClientChange(event.target.value)
setInvalidLogin(false)
}
const handlePasswordChange = event => {
onPasswordChange(event.target.value)
setInvalidLogin(false)
}
const handleRememberMeChange = () => {
onRememberMeChange(!rememberMeField)
}
const handleLogin = () => {
axios({
method: 'POST',
url: `${url}/api/login`,
data: {
username: clientField,
password: passwordField,
rememberMe: rememberMeField
},
options: {
withCredentials: true
},
headers: {
'Content-Type': 'application/json'
}
})
.then((res, err) => {
if (err) return
if (res) {
const status = res.status
const message = res.data.message
if (status === 200 && message === 'INPUT2FA')
handleLoginState(STATES.INPUT_2FA)
if (status === 200 && message === 'SETUP2FA')
handleLoginState(STATES.SETUP_2FA)
}
})
.catch(err => {
if (err.response && err.response.data) {
if (err.response.status === 403) setInvalidLogin(true)
}
})
const getErrorMsg = (formikErrors, formikTouched) => {
if (!formikErrors || !formikTouched) return null
if (mutationError) return 'Internal server error'
if (formikErrors.client && formikTouched.client) return formikErrors.client
if (formikErrors.password && formikTouched.password)
return formikErrors.password
if (invalidLogin) return 'Invalid login/password combination'
return null
}
return (
<>
<Label2 className={classes.inputLabel}>Client</Label2>
<TextInput
className={classes.input}
error={invalidLogin}
name="client-name"
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={values => {
setInvalidLogin(false)
onClientChange(values.client)
onPasswordChange(values.password)
onRememberMeChange(values.rememberMe)
login({
variables: {
username: values.client,
password: values.password
}
})
}}>
{({ errors, touched }) => (
<Form id="login-form">
<Field
name="client"
label="Client"
size="lg"
component={TextInput}
fullWidth
autoFocus
id="client-name"
type="text"
size="lg"
onChange={handleClientChange}
value={clientField}
/>
<Label2 className={classes.inputLabel}>Password</Label2>
<TextInput
className={classes.input}
error={invalidLogin}
error={getErrorMsg(errors, touched)}
onKeyUp={() => {
if (invalidLogin) setInvalidLogin(false)
}}
/>
<Field
name="password"
id="password"
type="password"
size="lg"
onChange={handlePasswordChange}
value={passwordField}
component={SecretInput}
label="Password"
fullWidth
error={getErrorMsg(errors, touched)}
onKeyUp={() => {
if (invalidLogin) setInvalidLogin(false)
}}
/>
<div className={classes.rememberMeWrapper}>
<Checkbox
<Field
name="rememberMe"
className={classes.checkbox}
id="remember-me"
onChange={handleRememberMeChange}
value={rememberMeField}
component={Checkbox}
/>
<Label2 className={classes.inputLabel}>Keep me logged in</Label2>
</div>
<div className={classes.footer}>
{invalidLogin && (
{getErrorMsg(errors, touched) && (
<P className={classes.errorMessage}>
Invalid login/password combination.
{getErrorMsg(errors, touched)}
</P>
)}
<Button
onClick={() => {
handleLogin()
}}
type="submit"
form="login-form"
buttonClassName={classes.loginButton}>
Login
</Button>
</div>
</Form>
)}
</Formik>
</>
)
}

View file

@ -1,103 +1,97 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Grid } from '@material-ui/core'
import Paper from '@material-ui/core/Paper'
import axios from 'axios'
import React, { useState, useEffect } from 'react'
import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import { useLocation, useHistory } from 'react-router-dom'
import * as Yup from 'yup'
import { Button } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/base'
import { SecretInput } from 'src/components/inputs/formik'
import { H2, Label2, P } from 'src/components/typography'
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
import styles from './Login.styles'
const useQuery = () => new URLSearchParams(useLocation().search)
const QueryParams = () => new URLSearchParams(useLocation().search)
const useStyles = makeStyles(styles)
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const VALIDATE_REGISTER_LINK = gql`
query validateRegisterLink($token: String!) {
validateRegisterLink(token: $token) {
username
role
}
}
`
const REGISTER = gql`
mutation register($username: String!, $password: String!, $role: String!) {
register(username: $username, password: $password, role: $role)
}
`
const validationSchema = Yup.object().shape({
password: Yup.string()
.required('A password is required')
.test(
'len',
'Your password must contain more than 8 characters',
val => val.length >= 8
),
confirmPassword: Yup.string().oneOf(
[Yup.ref('password'), null],
'Passwords must match'
)
})
const initialValues = {
password: '',
confirmPassword: ''
}
const Register = () => {
const classes = useStyles()
const history = useHistory()
const query = useQuery()
const [passwordField, setPasswordField] = useState('')
const [confirmPasswordField, setConfirmPasswordField] = useState('')
const [invalidPassword, setInvalidPassword] = useState(false)
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)
useEffect(() => {
validateQuery()
}, [])
const validateQuery = () => {
axios({
url: `${url}/api/register?t=${query.get('t')}`,
method: 'GET',
options: {
withCredentials: true
}
})
.then((res, err) => {
if (err) return
if (res && res.status === 200) {
const { error: queryError } = useQuery(VALIDATE_REGISTER_LINK, {
variables: { token: token },
onCompleted: ({ validateRegisterLink: info }) => {
setLoading(false)
if (res.data === 'The link has expired') setSuccess(false)
else {
if (!info) {
setSuccess(false)
} else {
setSuccess(true)
setUsername(res.data.username)
setRole(res.data.role)
setUsername(info.username)
setRole(info.role)
}
}
})
.catch(err => {
console.log(err)
history.push('/')
})
}
const handleRegister = () => {
if (!isValidPassword()) return setInvalidPassword(true)
axios({
url: `${url}/api/register`,
method: 'POST',
data: {
username: username,
password: passwordField,
role: role
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
onError: () => {
setLoading(false)
setSuccess(false)
}
})
.then((res, err) => {
if (err) return
if (res && res.status === 200) {
history.push('/wizard', { fromAuthRegister: true })
const [register, { error: mutationError }] = useMutation(REGISTER, {
onCompleted: ({ register: success }) => {
if (success) history.push('/wizard', { fromAuthRegister: true })
}
})
.catch(err => {
console.log(err)
history.push('/')
})
}
const isValidPassword = () => {
return passwordField === confirmPasswordField
}
const handlePasswordChange = event => {
setInvalidPassword(false)
setPasswordField(event.target.value)
}
const handleConfirmPasswordChange = event => {
setInvalidPassword(false)
setConfirmPasswordField(event.target.value)
const getErrorMsg = (formikErrors, formikTouched) => {
if (!formikErrors || !formikTouched) return null
if (queryError || mutationError) return 'Internal server error'
if (formikErrors.password && formikTouched.password)
return formikErrors.password
if (formikErrors.confirmPassword && formikTouched.confirmPassword)
return formikErrors.confirmPassword
return null
}
return (
@ -107,7 +101,6 @@ const Register = () => {
direction="column"
alignItems="center"
justify="center"
style={{ minHeight: '100vh' }}
className={classes.welcomeBackground}>
<Grid>
<div>
@ -118,49 +111,52 @@ const Register = () => {
<H2 className={classes.title}>Lamassu Admin</H2>
</div>
{!isLoading && wasSuccessful && (
<>
<Label2 className={classes.inputLabel}>
Insert a password
</Label2>
<TextInput
className={classes.input}
error={invalidPassword}
name="new-password"
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={values => {
register({
variables: {
username: username,
password: values.password,
role: role
}
})
}}>
{({ errors, touched }) => (
<Form id="register-form">
<Field
name="password"
label="Insert a password"
autoFocus
id="new-password"
type="password"
component={SecretInput}
size="lg"
onChange={handlePasswordChange}
value={passwordField}
/>
<Label2 className={classes.inputLabel}>
Confirm password
</Label2>
<TextInput
fullWidth
className={classes.input}
error={invalidPassword}
name="confirm-password"
id="confirm-password"
type="password"
/>
<Field
name="confirmPassword"
label="Confirm your password"
component={SecretInput}
size="lg"
onChange={handleConfirmPasswordChange}
value={confirmPasswordField}
fullWidth
/>
<div className={classes.footer}>
{invalidPassword && (
{getErrorMsg(errors, touched) && (
<P className={classes.errorMessage}>
Passwords do not match!
{getErrorMsg(errors, touched)}
</P>
)}
<Button
onClick={() => {
handleRegister()
}}
type="submit"
form="register-form"
buttonClassName={classes.loginButton}>
Done
</Button>
</div>
</>
</Form>
)}
</Formik>
)}
{!isLoading && !wasSuccessful && (
<>

View file

@ -1,8 +1,9 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Grid } from '@material-ui/core'
import Paper from '@material-ui/core/Paper'
import axios from 'axios'
import gql from 'graphql-tag'
import QRCode from 'qrcode.react'
import React, { useState, useEffect } from 'react'
import React, { useState } from 'react'
import { useLocation, useHistory } from 'react-router-dom'
import { ActionButton, Button } from 'src/components/buttons'
@ -13,16 +14,29 @@ import { primaryColor } from 'src/styling/variables'
import styles from './Login.styles'
const useQuery = () => new URLSearchParams(useLocation().search)
const QueryParams = () => new URLSearchParams(useLocation().search)
const useStyles = makeStyles(styles)
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const VALIDATE_RESET_2FA_LINK = gql`
query validateReset2FALink($token: String!) {
validateReset2FALink(token: $token) {
user_id
secret
otpauth
}
}
`
const RESET_2FA = gql`
mutation reset2FA($userID: ID!, $secret: String!, $code: String!) {
reset2FA(userID: $userID, secret: $secret, code: $code)
}
`
const Reset2FA = () => {
const classes = useStyles()
const history = useHistory()
const query = useQuery()
const token = QueryParams().get('t')
const [userID, setUserID] = useState(null)
const [isLoading, setLoading] = useState(true)
const [wasSuccessful, setSuccess] = useState(false)
@ -38,61 +52,37 @@ const Reset2FA = () => {
setInvalidToken(false)
}
useEffect(() => {
validateQuery()
}, [])
const validateQuery = () => {
axios({
url: `${url}/api/reset2fa?t=${query.get('t')}`,
method: 'GET',
options: {
withCredentials: true
}
})
.then((res, err) => {
if (err) return
if (res && res.status === 200) {
const { error: queryError } = useQuery(VALIDATE_RESET_2FA_LINK, {
variables: { token: token },
onCompleted: ({ validateReset2FALink: info }) => {
setLoading(false)
if (res.data === 'The link has expired') setSuccess(false)
else {
setUserID(res.data.userID)
setSecret(res.data.secret)
setOtpauth(res.data.otpauth)
if (!info) {
setSuccess(false)
} else {
setUserID(info.user_id)
setSecret(info.secret)
setOtpauth(info.otpauth)
setSuccess(true)
}
}
})
.catch(err => {
console.log(err)
history.push('/')
})
}
const handle2FAReset = () => {
axios({
url: `${url}/api/update2fa`,
method: 'POST',
data: {
userID: userID,
secret: secret,
code: twoFAConfirmation
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
onError: () => {
setLoading(false)
setSuccess(false)
}
})
.then((res, err) => {
if (err) return
if (res && res.status === 200) {
history.push('/')
const [reset2FA, { error: mutationError }] = useMutation(RESET_2FA, {
onCompleted: ({ reset2FA: success }) => {
success ? history.push('/') : setInvalidToken(true)
}
})
.catch(err => {
console.log(err)
setInvalidToken(true)
})
const getErrorMsg = () => {
if (mutationError || queryError) return 'Internal server error'
if (twoFAConfirmation.length !== 6 && invalidToken)
return 'The code should have 6 characters!'
if (invalidToken) return 'Code is invalid. Please try again.'
return null
}
return (
@ -102,7 +92,6 @@ const Reset2FA = () => {
direction="column"
alignItems="center"
justify="center"
style={{ minHeight: '100vh' }}
className={classes.welcomeBackground}>
<Grid>
<div>
@ -118,8 +107,7 @@ const Reset2FA = () => {
<Label2 className={classes.info2}>
To finish this process, please scan the following QR code
or insert the secret further below on an authentication
app of your choice, preferably Google Authenticator or
Authy.
app of your choice, such Google Authenticator or Authy.
</Label2>
</div>
<div className={classes.qrCodeWrapper}>
@ -150,17 +138,26 @@ const Reset2FA = () => {
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
</div>
<div className={classes.twofaFooter}>
{invalidToken && (
<P className={classes.errorMessage}>
Code is invalid. Please try again.
</P>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<Button
onClick={() => {
handle2FAReset()
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
reset2FA({
variables: {
userID: userID,
secret: secret,
code: twoFAConfirmation
}
})
}}
buttonClassName={classes.loginButton}>
Done

View file

@ -1,100 +1,94 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Grid } from '@material-ui/core'
import Paper from '@material-ui/core/Paper'
import axios from 'axios'
import React, { useState, useEffect } from 'react'
import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import { useLocation, useHistory } from 'react-router-dom'
import * as Yup from 'yup'
import { Button } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/base'
import { SecretInput } from 'src/components/inputs/formik/'
import { H2, Label2, P } from 'src/components/typography'
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
import styles from './Login.styles'
const useQuery = () => new URLSearchParams(useLocation().search)
const QueryParams = () => new URLSearchParams(useLocation().search)
const useStyles = makeStyles(styles)
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const VALIDATE_RESET_PASSWORD_LINK = gql`
query validateResetPasswordLink($token: String!) {
validateResetPasswordLink(token: $token) {
id
}
}
`
const RESET_PASSWORD = gql`
mutation resetPassword($userID: ID!, $newPassword: String!) {
resetPassword(userID: $userID, newPassword: $newPassword)
}
`
const validationSchema = Yup.object().shape({
password: Yup.string()
.required('A new password is required')
.test(
'len',
'New password must contain more than 8 characters',
val => val.length >= 8
),
confirmPassword: Yup.string().oneOf(
[Yup.ref('password'), null],
'Passwords must match'
)
})
const initialValues = {
password: '',
confirmPassword: ''
}
const ResetPassword = () => {
const classes = useStyles()
const history = useHistory()
const query = useQuery()
const [newPasswordField, setNewPasswordField] = useState('')
const [confirmPasswordField, setConfirmPasswordField] = useState('')
const [invalidPassword, setInvalidPassword] = useState(false)
const token = QueryParams().get('t')
const [userID, setUserID] = useState(null)
const [isLoading, setLoading] = useState(true)
const [wasSuccessful, setSuccess] = useState(false)
useEffect(() => {
validateQuery()
}, [])
const validateQuery = () => {
axios({
url: `${url}/api/resetpassword?t=${query.get('t')}`,
method: 'GET',
options: {
withCredentials: true
}
})
.then((res, err) => {
if (err) return
if (res && res.status === 200) {
useQuery(VALIDATE_RESET_PASSWORD_LINK, {
variables: { token: token },
onCompleted: ({ validateResetPasswordLink: info }) => {
setLoading(false)
if (res.data === 'The link has expired') setSuccess(false)
else {
if (!info) {
setSuccess(false)
} else {
setSuccess(true)
setUserID(res.data.userID)
setUserID(info.id)
}
}
})
.catch(err => {
console.log(err)
history.push('/')
})
}
const handlePasswordReset = () => {
if (!isValidPasswordChange()) return setInvalidPassword(true)
axios({
url: `${url}/api/updatepassword`,
method: 'POST',
data: {
userID: userID,
newPassword: newPasswordField
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
onError: () => {
setLoading(false)
setSuccess(false)
}
})
.then((res, err) => {
if (err) return
if (res && res.status === 200) {
history.push('/')
const [resetPassword, { error }] = useMutation(RESET_PASSWORD, {
onCompleted: ({ resetPassword: success }) => {
if (success) history.push('/')
}
})
.catch(err => {
console.log(err)
history.push('/')
})
}
const isValidPasswordChange = () => {
return newPasswordField === confirmPasswordField
}
const handleNewPasswordChange = event => {
setInvalidPassword(false)
setNewPasswordField(event.target.value)
}
const handleConfirmPasswordChange = event => {
setInvalidPassword(false)
setConfirmPasswordField(event.target.value)
const getErrorMsg = (formikErrors, formikTouched) => {
if (!formikErrors || !formikTouched) return null
if (error) return 'Internal server error'
if (formikErrors.password && formikTouched.password)
return formikErrors.password
if (formikErrors.confirmPassword && formikTouched.confirmPassword)
return formikErrors.confirmPassword
return null
}
return (
@ -104,7 +98,6 @@ const ResetPassword = () => {
direction="column"
alignItems="center"
justify="center"
style={{ minHeight: '100vh' }}
className={classes.welcomeBackground}>
<Grid>
<div>
@ -115,49 +108,51 @@ const ResetPassword = () => {
<H2 className={classes.title}>Lamassu Admin</H2>
</div>
{!isLoading && wasSuccessful && (
<>
<Label2 className={classes.inputLabel}>
Insert new password
</Label2>
<TextInput
className={classes.input}
error={invalidPassword}
name="new-password"
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={values => {
resetPassword({
variables: {
userID: userID,
newPassword: values.confirmPassword
}
})
}}>
{({ errors, touched }) => (
<Form id="reset-password">
<Field
name="password"
autoFocus
id="new-password"
type="password"
size="lg"
onChange={handleNewPasswordChange}
value={newPasswordField}
/>
<Label2 className={classes.inputLabel}>
Confirm new password
</Label2>
<TextInput
component={SecretInput}
label="New password"
fullWidth
className={classes.input}
error={invalidPassword}
name="confirm-password"
id="confirm-password"
type="password"
/>
<Field
name="confirmPassword"
size="lg"
onChange={handleConfirmPasswordChange}
value={confirmPasswordField}
component={SecretInput}
label="Confirm your password"
fullWidth
/>
<div className={classes.footer}>
{invalidPassword && (
{getErrorMsg(errors, touched) && (
<P className={classes.errorMessage}>
Passwords do not match!
{getErrorMsg(errors, touched)}
</P>
)}
<Button
onClick={() => {
handlePasswordReset()
}}
type="submit"
form="reset-password"
buttonClassName={classes.loginButton}>
Done
</Button>
</div>
</>
</Form>
)}
</Formik>
)}
{!isLoading && !wasSuccessful && (
<>

View file

@ -1,7 +1,8 @@
import { useMutation, useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import axios from 'axios'
import gql from 'graphql-tag'
import QRCode from 'qrcode.react'
import React, { useState, useEffect } from 'react'
import React, { useState } from 'react'
import { ActionButton, Button } from 'src/components/buttons'
import { CodeInput } from 'src/components/inputs/base'
@ -10,8 +11,30 @@ import { primaryColor } from 'src/styling/variables'
import styles from './Login.styles'
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const SETUP_2FA = gql`
mutation setup2FA(
$username: String!
$password: String!
$secret: String!
$codeConfirmation: String!
) {
setup2FA(
username: $username
password: $password
secret: $secret
codeConfirmation: $codeConfirmation
)
}
`
const GET_2FA_SECRET = gql`
query get2FASecret($username: String!, $password: String!) {
get2FASecret(username: $username, password: $password) {
secret
otpauth
}
}
`
const useStyles = makeStyles(styles)
@ -35,72 +58,26 @@ const Setup2FAState = ({
setInvalidToken(false)
}
useEffect(() => {
get2FASecret()
}, [])
const { error: queryError } = useQuery(GET_2FA_SECRET, {
variables: { username: clientField, password: passwordField },
onCompleted: ({ get2FASecret }) => {
setSecret(get2FASecret.secret)
setOtpauth(get2FASecret.otpauth)
}
})
const get2FASecret = () => {
axios({
method: 'POST',
url: `${url}/api/login/2fa/setup`,
data: {
username: clientField,
password: passwordField
},
options: {
withCredentials: true
},
headers: {
'Content-Type': 'application/json'
const [setup2FA, { error: mutationError }] = useMutation(SETUP_2FA, {
onCompleted: ({ setup2FA: success }) => {
success ? handleLoginState(STATES.LOGIN) : setInvalidToken(true)
}
})
.then((res, err) => {
if (err) return
if (res) {
setSecret(res.data.secret)
setOtpauth(res.data.otpauth)
}
})
.catch(err => {
if (err.response && err.response.data) {
if (err.response.status === 403) {
handleLoginState(STATES.LOGIN)
}
}
})
}
const save2FASecret = () => {
axios({
method: 'POST',
url: `${url}/api/login/2fa/save`,
data: {
username: clientField,
password: passwordField,
secret: secret,
code: twoFAConfirmation
},
options: {
withCredentials: true
},
headers: {
'Content-Type': 'application/json'
}
})
.then((res, err) => {
if (err) console.log(err)
if (res) {
const status = res.status
if (status === 200) handleLoginState(STATES.LOGIN)
}
})
.catch(err => {
if (err.response && err.response.data) {
if (err.response.status === 403) {
setInvalidToken(true)
}
}
})
const getErrorMsg = () => {
if (mutationError || queryError) return 'Internal server error'
if (twoFAConfirmation.length !== 6 && invalidToken)
return 'The code should have 6 characters!'
if (invalidToken) return 'Code is invalid. Please try again.'
return null
}
return (
@ -116,7 +93,7 @@ const Setup2FAState = ({
<Label2 className={classes.info2}>
To finish this process, please scan the following QR code or
insert the secret further below on an authentication app of your
choice, preferably Google Authenticator or Authy.
choice, such as Google Authenticator or Authy.
</Label2>
</div>
<div className={classes.qrCodeWrapper}>
@ -144,17 +121,27 @@ const Setup2FAState = ({
onChange={handle2FAChange}
numInputs={6}
error={invalidToken}
shouldAutoFocus
/>
</div>
<div className={classes.twofaFooter}>
{invalidToken && (
<P className={classes.errorMessage}>
Code is invalid. Please try again.
</P>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<Button
onClick={() => {
save2FASecret()
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
setup2FA({
variables: {
username: clientField,
password: passwordField,
secret: secret,
codeConfirmation: twoFAConfirmation
}
})
}}
buttonClassName={classes.loginButton}>
Done
@ -162,16 +149,7 @@ const Setup2FAState = ({
</div>
</>
) : (
// TODO: should maybe show a spinner here?
<div className={classes.twofaFooter}>
<Button
onClick={() => {
console.log('response should be arriving soon')
}}
buttonClassName={classes.loginButton}>
Generate Two Factor Authentication Secret
</Button>
</div>
<div></div>
)}
</>
)

View file

@ -16,6 +16,7 @@ const CopyToClipboard = ({
className,
buttonClassname,
children,
wrapperClassname,
...props
}) => {
const [anchorEl, setAnchorEl] = useState(null)
@ -38,7 +39,7 @@ const CopyToClipboard = ({
const id = open ? 'simple-popper' : undefined
return (
<div className={classes.wrapper}>
<div className={classnames(classes.wrapper, wrapperClassname)}>
{children && (
<>
<div className={classnames(classes.address, className)}>

View file

@ -1,24 +1,18 @@
/* eslint-disable prettier/prettier */
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Box, Chip } from '@material-ui/core'
import axios from 'axios'
import gql from 'graphql-tag'
// import moment from 'moment'
import * as R from 'ramda'
import React, { useState, useContext } from 'react'
// import parser from 'ua-parser-js'
import { AppContext } from 'src/App'
import { Link /*, IconButton */ } from 'src/components/buttons'
import { Link } from 'src/components/buttons'
import { Switch } from 'src/components/inputs'
import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable'
// import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
import styles from './UserManagement.styles'
import ChangeRoleModal from './modals/ChangeRoleModal'
import CreateUserModal from './modals/CreateUserModal'
// import DeleteUserModal from './modals/DeleteUserModal'
import EnableUserModal from './modals/EnableUserModal'
import Input2FAModal from './modals/Input2FAModal'
import Reset2FAModal from './modals/Reset2FAModal'
@ -26,9 +20,6 @@ import ResetPasswordModal from './modals/ResetPasswordModal'
const useStyles = makeStyles(styles)
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const GET_USERS = gql`
query users {
users {
@ -43,14 +34,6 @@ const GET_USERS = gql`
}
`
/* const DELETE_USERS = gql`
mutation deleteUser($id: ID!) {
deleteUser(id: $id) {
id
}
}
` */
const CHANGE_USER_ROLE = gql`
mutation changeUserRole($id: ID!, $newRole: String!) {
changeUserRole(id: $id, newRole: $newRole) {
@ -67,6 +50,26 @@ const TOGGLE_USER_ENABLE = gql`
}
`
const CREATE_RESET_PASSWORD_TOKEN = gql`
mutation createResetPasswordToken($userID: ID!) {
createResetPasswordToken(userID: $userID) {
token
user_id
expire
}
}
`
const CREATE_RESET_2FA_TOKEN = gql`
mutation createReset2FAToken($userID: ID!) {
createReset2FAToken(userID: $userID) {
token
user_id
expire
}
}
`
const Users = () => {
const classes = useStyles()
@ -74,10 +77,6 @@ const Users = () => {
const { data: userResponse } = useQuery(GET_USERS)
/* const [deleteUser] = useMutation(DELETE_USERS, {
refetchQueries: () => ['users']
}) */
const [changeUserRole] = useMutation(CHANGE_USER_ROLE, {
refetchQueries: () => ['users']
})
@ -86,6 +85,22 @@ const Users = () => {
refetchQueries: () => ['users']
})
const [createResetPasswordToken] = useMutation(CREATE_RESET_PASSWORD_TOKEN, {
onCompleted: ({ createResetPasswordToken: token }) => {
setResetPasswordUrl(
`https://localhost:3001/resetpassword?t=${token.token}`
)
toggleResetPasswordModal()
}
})
const [createReset2FAToken] = useMutation(CREATE_RESET_2FA_TOKEN, {
onCompleted: ({ createReset2FAToken: token }) => {
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
toggleReset2FAModal()
}
})
const [userInfo, setUserInfo] = useState(null)
const [showCreateUserModal, setShowCreateUserModal] = useState(false)
@ -102,81 +117,18 @@ const Users = () => {
const toggleReset2FAModal = () => setShowReset2FAModal(!showReset2FAModal)
const [showRoleModal, setShowRoleModal] = useState(false)
const toggleRoleModal = () =>
setShowRoleModal(!showRoleModal)
const toggleRoleModal = () => setShowRoleModal(!showRoleModal)
const [showEnableUserModal, setShowEnableUserModal] = useState(false)
const toggleEnableUserModal = () =>
setShowEnableUserModal(!showEnableUserModal)
/* const [showDeleteUserModal, setShowDeleteUserModal] = useState(false)
const toggleDeleteUserModal = () =>
setShowDeleteUserModal(!showDeleteUserModal) */
const [showInputConfirmModal, setShowInputConfirmModal] = useState(false)
const toggleInputConfirmModal = () =>
setShowInputConfirmModal(!showInputConfirmModal)
const [action, setAction] = useState(null)
const requestNewPassword = userID => {
axios({
method: 'POST',
url: `${url}/api/resetpassword`,
data: {
userID: userID
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
.then((res, err) => {
if (err) return
if (res) {
const status = res.status
if (status === 200) {
const token = res.data.token
setResetPasswordUrl(
`https://localhost:3001/resetpassword?t=${token.token}`
)
toggleResetPasswordModal()
}
}
})
.catch(err => {
if (err) console.log('error')
})
}
const requestNew2FA = userID => {
axios({
method: 'POST',
url: `${url}/api/reset2fa`,
data: {
userID: userID
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
.then((res, err) => {
if (err) return
if (res) {
const status = res.status
if (status === 200) {
const token = res.data.token
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
toggleReset2FAModal()
}
}
})
.catch(err => {
if (err) console.log('error')
})
}
const elements = [
{
header: 'Login',
@ -248,11 +200,21 @@ const Users = () => {
className={classes.actionChip}
onClick={() => {
setUserInfo(u)
if(u.role === 'superuser') {
setAction(() => requestNewPassword.bind(null, u.id))
if (u.role === 'superuser') {
setAction(() =>
createResetPasswordToken.bind(null, {
variables: {
userID: u.id
}
})
)
toggleInputConfirmModal()
} else {
requestNewPassword(u.id)
createResetPasswordToken({
variables: {
userID: u.id
}
})
}
}}
/>
@ -262,11 +224,21 @@ const Users = () => {
className={classes.actionChip}
onClick={() => {
setUserInfo(u)
if(u.role === 'superuser') {
setAction(() => requestNew2FA.bind(null, u.id))
if (u.role === 'superuser') {
setAction(() => () =>
createReset2FAToken({
variables: {
userID: u.id
}
})
)
toggleInputConfirmModal()
} else {
requestNew2FA(u.id)
createReset2FAToken({
variables: {
userID: u.id
}
})
}
}}
/>
@ -274,18 +246,6 @@ const Users = () => {
)
}
},
/* {
header: 'Actions',
width: 535,
textAlign: 'left',
size: 'sm',
view: u => {
const ua = parser(u.last_accessed_from)
return u.last_accessed_from
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
: `No Record`
}
}, */
{
header: 'Enabled',
width: 100,
@ -302,22 +262,7 @@ const Users = () => {
value={u.enabled}
/>
)
}/* ,
{
header: 'Delete',
width: 100,
textAlign: 'center',
size: 'sm',
view: u => (
<IconButton
onClick={() => {
setUserInfo(u)
toggleDeleteUserModal()
}}>
<DeleteIcon />
</IconButton>
)
} */
}
]
return (
@ -366,14 +311,6 @@ const Users = () => {
inputConfirmToggle={toggleInputConfirmModal}
setAction={setAction}
/>
{/* <DeleteUserModal
showModal={showDeleteUserModal}
toggleModal={toggleDeleteUserModal}
user={userInfo}
confirm={deleteUser}
inputConfirmToggle={toggleInputConfirmModal}
setAction={setAction}
/> */}
<Input2FAModal
showModal={showInputConfirmModal}
toggleModal={toggleInputConfirmModal}

View file

@ -9,7 +9,9 @@ import {
const styles = {
footer: {
margin: [['auto', 0, spacer * 3, 'auto']]
display: 'flex',
flexDirection: 'row',
margin: [['auto', 0, spacer * 3, 0]]
},
modalTitle: {
marginTop: -5,
@ -42,9 +44,8 @@ const styles = {
},
copyToClipboard: {
marginLeft: 'auto',
paddingTop: 6,
paddingLeft: 15,
marginRight: -11
paddingTop: 7,
marginRight: -5
},
chip: {
backgroundColor: subheaderColor,
@ -63,10 +64,12 @@ const styles = {
},
addressWrapper: {
backgroundColor: subheaderColor,
marginTop: 8
marginTop: 8,
height: 35
},
address: {
margin: `${spacer * 1.5}px ${spacer * 3}px`
margin: `0px ${spacer * 2}px 0px ${spacer * 2}px`,
paddingRight: -15
},
errorMessage: {
fontFamily: fontSecondary,
@ -75,6 +78,33 @@ const styles = {
codeContainer: {
marginTop: 15,
marginBottom: 15
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%'
},
submit: {
margin: [['auto', 0, 0, 'auto']]
},
error: {
color: errorColor
},
link: {
position: 'absolute',
top: 10,
left: 0,
bottom: '-20px',
right: '-20px',
whiteSpace: 'nowrap',
overflowX: 'auto',
width: '92.5%'
},
test1: {
width: '100%',
height: '100%',
overflow: 'hidden',
position: 'relative'
}
}

View file

@ -3,7 +3,7 @@ import React from 'react'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
import { H2, Info3 } from 'src/components/typography'
import { Info2, P } from 'src/components/typography'
import styles from '../UserManagement.styles'
@ -28,18 +28,21 @@ const ChangeRoleModal = ({
{showModal && (
<Modal
closeOnBackdropClick={true}
width={600}
height={275}
width={450}
height={250}
handleClose={handleClose}
open={true}>
<H2 className={classes.modalTitle}>Change {user.username}'s role?</H2>
<Info3 className={classes.info}>
<Info2 className={classes.modalTitle}>
Change {user.username}'s role?
</Info2>
<P className={classes.info}>
You are about to alter {user.username}'s role. This will change this
user's permission to access certain resources.
</Info3>
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
</P>
<P className={classes.info}>Do you wish to proceed?</P>
<div className={classes.footer}>
<Button
className={classes.submit}
onClick={() => {
setAction(() =>
confirm.bind(null, {
@ -52,7 +55,7 @@ const ChangeRoleModal = ({
inputConfirmToggle()
handleClose()
}}>
Finish
Confirm
</Button>
</div>
</Modal>

View file

@ -1,28 +1,48 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import axios from 'axios'
import classnames from 'classnames'
import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import * as Yup from 'yup'
import ErrorMessage from 'src/components/ErrorMessage'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs'
import { TextInput } from 'src/components/inputs/base'
import { TextInput, RadioGroup } from 'src/components/inputs/formik'
import { H1, H2, H3, Info3, Mono } from 'src/components/typography'
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
import styles from '../UserManagement.styles'
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const useStyles = makeStyles(styles)
const CREATE_USER = gql`
mutation createRegisterToken($username: String!, $role: String!) {
createRegisterToken(username: $username, role: $role) {
token
expire
}
}
`
const validationSchema = Yup.object().shape({
username: Yup.string()
.email('Username field should be in an email format!')
.required('Username field is required!'),
role: Yup.string().required('Role field is required!')
})
const initialValues = {
username: '',
role: ''
}
const CreateUserModal = ({ showModal, toggleModal }) => {
const classes = useStyles()
const [usernameField, setUsernameField] = useState('')
const [roleField, setRoleField] = useState('')
const [createUserURL, setCreateUserURL] = useState(null)
const [invalidUser, setInvalidUser] = useState(false)
const radioOptions = [
{
@ -35,59 +55,27 @@ const CreateUserModal = ({ showModal, toggleModal }) => {
}
]
const handleUsernameChange = event => {
if (event.target.value === '') {
setInvalidUser(false)
}
setUsernameField(event.target.value)
}
const handleRoleChange = event => {
setRoleField(event.target.value)
}
const handleClose = () => {
setUsernameField('')
setRoleField('')
setInvalidUser(false)
setCreateUserURL(null)
toggleModal()
}
const handleCreateUser = () => {
const username = usernameField.trim()
if (username === '') {
setInvalidUser(true)
return
}
axios({
method: 'POST',
url: `${url}/api/createuser`,
data: {
username: username,
role: roleField
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
.then((res, err) => {
if (err) return
if (res) {
const status = res.status
const message = res.data.message
if (status === 200 && message) setInvalidUser(true)
if (status === 200 && !message) {
const token = res.data.token
const [createUser, { error }] = useMutation(CREATE_USER, {
onCompleted: ({ createRegisterToken: token }) => {
setCreateUserURL(`https://localhost:3001/register?t=${token.token}`)
}
}
})
.catch(err => {
if (err) console.log('error')
const roleClass = (formikErrors, formikTouched) => ({
[classes.error]: formikErrors.role && formikTouched.role
})
const getErrorMsg = (formikErrors, formikTouched) => {
if (!formikErrors || !formikTouched) return null
if (error) return 'Internal server error'
if (formikErrors.username && formikTouched.username)
return formikErrors.username
return null
}
return (
@ -99,38 +87,60 @@ const CreateUserModal = ({ showModal, toggleModal }) => {
height={400}
handleClose={handleClose}
open={true}>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={values => {
setUsernameField(values.username)
createUser({
variables: { username: values.username, role: values.role }
})
}}>
{({ errors, touched }) => (
<Form id="register-user-form" className={classes.form}>
<H1 className={classes.modalTitle}>Create new user</H1>
<H3 className={classes.modalLabel1}>User login</H3>
<TextInput
error={invalidUser}
<Field
component={TextInput}
name="username"
autoFocus
id="username"
type="text"
size="lg"
width={338}
onChange={handleUsernameChange}
value={usernameField}
autoFocus
label="User login"
/>
<H3 className={classes.modalLabel2}>Role</H3>
<RadioGroup
name="userrole"
value={roleField}
options={radioOptions}
onChange={handleRoleChange}
className={classes.radioGroup}
<H3
className={classnames(
roleClass(errors, touched),
classes.modalLabel2
)}>
Role
</H3>
<Field
component={RadioGroup}
name="role"
labelClassName={classes.radioLabel}
className={classes.radioGroup}
options={radioOptions}
/>
<div className={classes.footer}>
<Button onClick={handleCreateUser}>Finish</Button>
{getErrorMsg(errors, touched) && (
<ErrorMessage>{getErrorMsg(errors, touched)}</ErrorMessage>
)}
<Button
type="submit"
form="register-user-form"
className={classes.submit}>
Finish
</Button>
</div>
</Form>
)}
</Formik>
</Modal>
)}
{showModal && createUserURL && (
<Modal
closeOnBackdropClick={true}
width={600}
height={215}
height={275}
handleClose={handleClose}
open={true}>
<H2 className={classes.modalTitle}>Creating {usernameField}...</H2>

View file

@ -3,7 +3,7 @@ import React from 'react'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
import { H2, Info3 } from 'src/components/typography'
import { Info2, P } from 'src/components/typography'
import styles from '../UserManagement.styles'
@ -32,16 +32,17 @@ const DeleteUserModal = ({
height={275}
handleClose={handleClose}
open={true}>
<H2 className={classes.modalTitle}>Delete {user.username}?</H2>
<Info3 className={classes.info}>
<Info2 className={classes.modalTitle}>Delete {user.username}?</Info2>
<P className={classes.info}>
You are about to delete {user.username}. This will remove existent
sessions and revoke this user's permissions to access the system.
</Info3>
<Info3 className={classes.info}>
</P>
<P className={classes.info}>
This is a <b>PERMANENT</b> operation. Do you wish to proceed?
</Info3>
</P>
<div className={classes.footer}>
<Button
className={classes.submit}
onClick={() => {
if (user.role === 'superuser') {
setAction(() =>
@ -61,7 +62,7 @@ const DeleteUserModal = ({
}
handleClose()
}}>
Finish
Confirm
</Button>
</div>
</Modal>

View file

@ -3,7 +3,7 @@ import React from 'react'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
import { H2, Info3 } from 'src/components/typography'
import { Info2, P } from 'src/components/typography'
import styles from '../UserManagement.styles'
@ -28,34 +28,39 @@ const EnableUserModal = ({
{showModal && (
<Modal
closeOnBackdropClick={true}
width={600}
width={450}
height={275}
handleClose={handleClose}
open={true}>
{!user.enabled && (
<>
<H2 className={classes.modalTitle}>Enable {user.username}?</H2>
<Info3 className={classes.info}>
<Info2 className={classes.modalTitle}>
Enable {user.username}?
</Info2>
<P className={classes.info}>
You are about to enable {user.username} into the system,
activating previous eligible sessions and grant permissions to
access the system.
</Info3>
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
</P>
<P className={classes.info}>Do you wish to proceed?</P>
</>
)}
{user.enabled && (
<>
<H2 className={classes.modalTitle}>Disable {user.username}?</H2>
<Info3 className={classes.info}>
<Info2 className={classes.modalTitle}>
Disable {user.username}?
</Info2>
<P className={classes.info}>
You are about to disable {user.username} from the system,
deactivating previous eligible sessions and removing permissions
to access the system.
</Info3>
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
</P>
<P className={classes.info}>Do you wish to proceed?</P>
</>
)}
<div className={classes.footer}>
<Button
className={classes.submit}
onClick={() => {
if (user.role === 'superuser') {
setAction(() =>
@ -75,7 +80,7 @@ const EnableUserModal = ({
}
handleClose()
}}>
Finish
Confirm
</Button>
</div>
</Modal>

View file

@ -1,19 +1,23 @@
import { useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import axios from 'axios'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
import { CodeInput } from 'src/components/inputs/base'
import { H2, Info3, P } from 'src/components/typography'
import { Info2, P } from 'src/components/typography'
import styles from '../UserManagement.styles'
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const useStyles = makeStyles(styles)
const CONFIRM_2FA = gql`
query confirm2FA($code: String!) {
confirm2FA(code: $code)
}
`
const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
const classes = useStyles()
@ -31,32 +35,23 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
toggleModal()
}
const handleActionConfirm = () => {
axios({
method: 'POST',
url: `${url}/api/confirm2fa`,
data: {
code: twoFACode
},
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
.then((res, err) => {
if (err) return
if (res) {
const status = res.status
if (status === 200) {
const [confirm2FA, { error: queryError }] = useLazyQuery(CONFIRM_2FA, {
onCompleted: ({ confirm2FA: success }) => {
if (!success) {
setInvalidCode(true)
} else {
action()
handleClose()
}
}
})
.catch(err => {
const errStatus = err.response.status
if (errStatus === 401) setInvalidCode(true)
})
const getErrorMsg = () => {
if (queryError) return 'Internal server error'
if (twoFACode.length !== 6 && invalidCode)
return 'The code should have 6 characters!'
if (invalidCode) return 'Code is invalid. Please try again.'
return null
}
return (
@ -64,15 +59,15 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
{showModal && (
<Modal
closeOnBackdropClick={true}
width={600}
height={400}
width={500}
height={350}
handleClose={handleClose}
open={true}>
<H2 className={classes.modalTitle}>Confirm action</H2>
<Info3 className={classes.info}>
Please confirm this action by placing your two-factor authentication
code below.
</Info3>
<Info2 className={classes.modalTitle}>Confirm action</Info2>
<P className={classes.info}>
To make changes on this user, please confirm this action by entering
your two-factor authentication code below.
</P>
<CodeInput
name="2fa"
value={twoFACode}
@ -82,13 +77,21 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
containerStyle={classes.codeContainer}
shouldAutoFocus
/>
{invalidCode && (
<P className={classes.errorMessage}>
Code is invalid. Please try again.
</P>
{getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P>
)}
<div className={classes.footer}>
<Button onClick={handleActionConfirm}>Finish</Button>
<Button
className={classes.submit}
onClick={() => {
if (twoFACode.length !== 6) {
setInvalidCode(true)
return
}
confirm2FA({ variables: { code: twoFACode } })
}}>
Confirm
</Button>
</div>
</Modal>
)}

View file

@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import Modal from 'src/components/Modal'
import { H2, Info3, Mono } from 'src/components/typography'
import { Info2, P, Mono } from 'src/components/typography'
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
import styles from '../UserManagement.styles'
@ -21,19 +21,24 @@ const Reset2FAModal = ({ showModal, toggleModal, reset2FAURL, user }) => {
{showModal && (
<Modal
closeOnBackdropClick={true}
width={600}
height={215}
width={500}
height={200}
handleClose={handleClose}
open={true}>
<H2 className={classes.modalTitle}>Reset 2FA for {user.username}</H2>
<Info3 className={classes.info}>
<Info2 className={classes.modalTitle}>
Reset 2FA for {user.username}
</Info2>
<P className={classes.info}>
Safely share this link with {user.username} for a two-factor
authentication reset.
</Info3>
</P>
<div className={classes.addressWrapper}>
<Mono className={classes.address}>
<strong>
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
<CopyToClipboard
className={classes.link}
buttonClassname={classes.copyToClipboard}
wrapperClassname={classes.test1}>
{reset2FAURL}
</CopyToClipboard>
</strong>

View file

@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import Modal from 'src/components/Modal'
import { H2, Info3, Mono } from 'src/components/typography'
import { Info2, P, Mono } from 'src/components/typography'
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
import styles from '../UserManagement.styles'
@ -26,20 +26,23 @@ const ResetPasswordModal = ({
{showModal && (
<Modal
closeOnBackdropClick={true}
width={600}
height={215}
width={500}
height={180}
handleClose={handleClose}
open={true}>
<H2 className={classes.modalTitle}>
<Info2 className={classes.modalTitle}>
Reset password for {user.username}
</H2>
<Info3 className={classes.info}>
</Info2>
<P className={classes.info}>
Safely share this link with {user.username} for a password reset.
</Info3>
</P>
<div className={classes.addressWrapper}>
<Mono className={classes.address}>
<strong>
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
<CopyToClipboard
className={classes.link}
buttonClassname={classes.copyToClipboard}
wrapperClassname={classes.test1}>
{resetPasswordURL}
</CopyToClipboard>
</strong>

View file

@ -6,9 +6,10 @@ import {
MuiThemeProvider,
makeStyles
} from '@material-ui/core/styles'
import { axios } from '@use-hooks/axios'
import { create } from 'jss'
import extendJss from 'jss-plugin-extend'
import React, { useContext, useState } from 'react'
import React, { useContext, useEffect, useState } from 'react'
import {
useLocation,
useHistory,
@ -72,7 +73,7 @@ const Main = () => {
const classes = useStyles()
const location = useLocation()
const history = useHistory()
const { wizardTested } = useContext(AppContext)
const { wizardTested, userData } = useContext(AppContext)
const route = location.pathname
@ -91,7 +92,9 @@ const Main = () => {
return (
<div className={classes.root}>
{!is404 && wizardTested && <Header tree={tree} />}
{!is404 && wizardTested && userData && (
<Header tree={tree} user={userData} />
)}
<main className={classes.wrapper}>
{sidebar && !is404 && wizardTested && (
<TitleSection title={parent.title}></TitleSection>
@ -117,9 +120,36 @@ const Main = () => {
const App = () => {
const [wizardTested, setWizardTested] = useState(false)
const [userData, setUserData] = useState(null)
const [loading, setLoading] = useState(true)
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
useEffect(() => {
getUserData()
}, [])
const getUserData = () => {
axios({
method: 'GET',
url: `${url}/user-data`,
withCredentials: true
})
.then(res => {
setLoading(false)
if (res.status === 200) setUserData(res.data.user)
})
.catch(err => {
setLoading(false)
if (err.status === 403) setUserData(null)
})
}
return (
<AppContext.Provider value={{ wizardTested, setWizardTested }}>
<AppContext.Provider
value={{ wizardTested, setWizardTested, userData, setUserData }}>
{!loading && (
<Router>
<ApolloProvider>
<StylesProvider jss={jss}>
@ -130,6 +160,7 @@ const App = () => {
</StylesProvider>
</ApolloProvider>
</Router>
)}
</AppContext.Provider>
)
}

View file

@ -38,6 +38,7 @@ import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
import ServerLogs from 'src/pages/ServerLogs'
import Services from 'src/pages/Services/Services'
// import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
import SessionManagement from 'src/pages/SessionManagement/SessionManagement'
import Transactions from 'src/pages/Transactions/Transactions'
import Triggers from 'src/pages/Triggers'
@ -247,6 +248,7 @@ const tree = [
key: 'promo-codes',
label: 'Promo Codes',
route: '/compliance/loyalty/codes',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: PromoCodes
},
{
@ -397,12 +399,12 @@ const Routes = () => {
<PrivateRoute path="/machines" component={Machines} />
<PrivateRoute path="/wizard" component={Wizard} />
<Route path="/register" component={Register} />
{/* <Route path="/configmigration" component={ConfigMigration} /> */}
<PublicRoute path="/login" restricted component={Login} />
<Route path="/resetpassword" component={ResetPassword} />
<Route path="/reset2fa" component={Reset2FA} />
{/* <Route path="/configmigration" component={ConfigMigration} /> */}
{getFilteredRoutes().map(({ route, component: Page, key }) => (
<Route path={route} key={key}>
<PrivateRoute path={route} key={key}>
<Transition
className={classes.wrapper}
{...transitionProps}
@ -417,7 +419,7 @@ const Routes = () => {
</div>
}
/>
</Route>
</PrivateRoute>
))}
<Route path="/404" />
<Route path="*">

View file

@ -7,7 +7,7 @@ import { HttpLink } from 'apollo-link-http'
import React, { useContext } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { AppContext } from 'src/App'
import AppContext from 'src/AppContext'
const URI =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''

47
package-lock.json generated
View file

@ -24,6 +24,14 @@
"zen-observable": "^0.8.14"
},
"dependencies": {
"@wry/equality": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.1.tgz",
"integrity": "sha512-8/Ftr3jUZ4EXhACfSwPIfNsE8V6WKesdjp+Dxi78Bej6qlasAxiz0/F8j0miACRj9CL4vC5Y5FsfwwEYAuhWbg==",
"requires": {
"tslib": "^1.14.1"
}
},
"symbol-observable": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz",
@ -512,6 +520,13 @@
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.10.4"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
"@babel/plugin-syntax-nullish-coalescing-operator": {
@ -538,6 +553,13 @@
"integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
"requires": {
"@babel/helper-plugin-utils": "^7.8.0"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
"@babel/plugin-syntax-optional-catch-binding": {
@ -757,6 +779,11 @@
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
@ -768,6 +795,18 @@
"@babel/code-frame": "^7.10.4",
"@babel/parser": "^7.12.7",
"@babel/types": "^7.12.7"
},
"dependencies": {
"is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
},
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
}
}
},
"@babel/traverse": {
@ -2531,14 +2570,6 @@
"tslib": "^1.14.1"
}
},
"@wry/equality": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.1.tgz",
"integrity": "sha512-8/Ftr3jUZ4EXhACfSwPIfNsE8V6WKesdjp+Dxi78Bej6qlasAxiz0/F8j0miACRj9CL4vC5Y5FsfwwEYAuhWbg==",
"requires": {
"tslib": "^1.14.1"
}
},
"@wry/trie": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.2.1.tgz",