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 db = require('../db')
const users = require('../users') 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 devMode = require('minimist')(process.argv.slice(2)).dev
const idPhotoCardBasedir = _.get('idPhotoCardDir', options) const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
@ -64,8 +66,7 @@ const apolloServer = new ApolloServer({
typeDefs, typeDefs,
resolvers, resolvers,
schemaDirectives: { schemaDirectives: {
auth: AuthDirective, auth: AuthDirective
superuser: SuperuserDirective
}, },
playground: false, playground: false,
introspection: false, introspection: false,
@ -74,7 +75,8 @@ const apolloServer = new ApolloServer({
return error return error
}, },
context: async ({ req }) => { context: async ({ req }) => {
if (!req.session.user) throw new AuthenticationError('Authentication failed') if (!req.session.user) return { req }
const user = await users.verifyAndUpdateUser( const user = await users.verifyAndUpdateUser(
req.session.user.id, req.session.user.id,
req.headers['user-agent'] || 'Unknown', req.headers['user-agent'] || 'Unknown',
@ -87,7 +89,8 @@ const apolloServer = new ApolloServer({
req.session.lastUsed = new Date(Date.now()).toISOString() req.session.lastUsed = new Date(Date.now()).toISOString()
req.session.user.id = user.id req.session.user.id = user.id
req.session.user.role = user.role 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('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false }))
app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false })) app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false }))
app.use('/api', register) app.use(authRouter)
require('./routes/auth')(app)
// Everything not on graphql or api/register is redirected to the front-end // 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'))) 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 settings = require('./settings.resolver')
const status = require('./status.resolver') const status = require('./status.resolver')
const transaction = require('./transaction.resolver') const transaction = require('./transaction.resolver')
const user = require('./users.resolver')
const version = require('./version.resolver') const version = require('./version.resolver')
const resolvers = [ const resolvers = [
@ -35,6 +36,7 @@ const resolvers = [
settings, settings,
status, status,
transaction, transaction,
user,
version 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 settings = require('./settings.type')
const status = require('./status.type') const status = require('./status.type')
const transaction = require('./transaction.type') const transaction = require('./transaction.type')
const user = require('./users.type')
const version = require('./version.type') const version = require('./version.type')
const types = [ const types = [
@ -35,6 +36,7 @@ const types = [
settings, settings,
status, status,
transaction, transaction,
user,
version 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,259 +1,17 @@
const otplib = require('otplib') const express = require('express')
const bcrypt = require('bcrypt') const router = express.Router()
const users = require('../../users') const getUserData = function (req, res, next) {
const login = require('../login') const lidCookie = req.cookies && req.cookies.lid
if (!lidCookie) {
res.sendStatus(403)
return
}
async function isValidUser (username, password) { const user = req.session.user
const hashedPassword = await login.checkUser(username) return res.status(200).json({ message: 'Success', user: user })
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) { router.get('/user-data', getUserData)
app.post('/api/login', function (req, res, next) {
const usernameInput = req.body.username
const passwordInput = req.body.password
isValidUser(usernameInput, passwordInput).then(user => { module.exports = router
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 lidCookie = req.cookies && req.cookies.lid
if (!lidCookie) {
res.sendStatus(403)
return
}
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)
})
})
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,128 +1,134 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles' 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 React, { useState } from 'react'
import * as Yup from 'yup'
import { Button } from 'src/components/buttons' 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 { Label2, P } from 'src/components/typography'
import styles from './Login.styles' import styles from './Login.styles'
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const useStyles = makeStyles(styles) 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 = ({ const LoginState = ({
clientField,
onClientChange, onClientChange,
passwordField,
onPasswordChange, onPasswordChange,
rememberMeField,
onRememberMeChange, onRememberMeChange,
STATES, STATES,
handleLoginState handleLoginState
}) => { }) => {
const classes = useStyles() 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 [invalidLogin, setInvalidLogin] = useState(false)
const handleClientChange = event => { const getErrorMsg = (formikErrors, formikTouched) => {
onClientChange(event.target.value) if (!formikErrors || !formikTouched) return null
setInvalidLogin(false) if (mutationError) return 'Internal server error'
} if (formikErrors.client && formikTouched.client) return formikErrors.client
if (formikErrors.password && formikTouched.password)
const handlePasswordChange = event => { return formikErrors.password
onPasswordChange(event.target.value) if (invalidLogin) return 'Invalid login/password combination'
setInvalidLogin(false) return null
}
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)
}
})
} }
return ( return (
<> <>
<Label2 className={classes.inputLabel}>Client</Label2> <Formik
<TextInput validationSchema={validationSchema}
className={classes.input} initialValues={initialValues}
error={invalidLogin} onSubmit={values => {
name="client-name" setInvalidLogin(false)
autoFocus onClientChange(values.client)
id="client-name" onPasswordChange(values.password)
type="text" onRememberMeChange(values.rememberMe)
size="lg" login({
onChange={handleClientChange} variables: {
value={clientField} username: values.client,
/> password: values.password
<Label2 className={classes.inputLabel}>Password</Label2> }
<TextInput })
className={classes.input} }}>
error={invalidLogin} {({ errors, touched }) => (
name="password" <Form id="login-form">
id="password" <Field
type="password" name="client"
size="lg" label="Client"
onChange={handlePasswordChange} size="lg"
value={passwordField} component={TextInput}
/> fullWidth
<div className={classes.rememberMeWrapper}> autoFocus
<Checkbox className={classes.input}
className={classes.checkbox} error={getErrorMsg(errors, touched)}
id="remember-me" onKeyUp={() => {
onChange={handleRememberMeChange} if (invalidLogin) setInvalidLogin(false)
value={rememberMeField} }}
/> />
<Label2 className={classes.inputLabel}>Keep me logged in</Label2> <Field
</div> name="password"
<div className={classes.footer}> size="lg"
{invalidLogin && ( component={SecretInput}
<P className={classes.errorMessage}> label="Password"
Invalid login/password combination. fullWidth
</P> error={getErrorMsg(errors, touched)}
onKeyUp={() => {
if (invalidLogin) setInvalidLogin(false)
}}
/>
<div className={classes.rememberMeWrapper}>
<Field
name="rememberMe"
className={classes.checkbox}
component={Checkbox}
/>
<Label2 className={classes.inputLabel}>Keep me logged in</Label2>
</div>
<div className={classes.footer}>
{getErrorMsg(errors, touched) && (
<P className={classes.errorMessage}>
{getErrorMsg(errors, touched)}
</P>
)}
<Button
type="submit"
form="login-form"
buttonClassName={classes.loginButton}>
Login
</Button>
</div>
</Form>
)} )}
<Button </Formik>
onClick={() => {
handleLogin()
}}
buttonClassName={classes.loginButton}>
Login
</Button>
</div>
</> </>
) )
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,24 +1,18 @@
/* eslint-disable prettier/prettier */
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Box, Chip } from '@material-ui/core' import { makeStyles, Box, Chip } from '@material-ui/core'
import axios from 'axios'
import gql from 'graphql-tag' import gql from 'graphql-tag'
// import moment from 'moment'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
// import parser from 'ua-parser-js'
import { AppContext } from 'src/App' 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 { Switch } from 'src/components/inputs'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable' 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 styles from './UserManagement.styles'
import ChangeRoleModal from './modals/ChangeRoleModal' import ChangeRoleModal from './modals/ChangeRoleModal'
import CreateUserModal from './modals/CreateUserModal' import CreateUserModal from './modals/CreateUserModal'
// import DeleteUserModal from './modals/DeleteUserModal'
import EnableUserModal from './modals/EnableUserModal' import EnableUserModal from './modals/EnableUserModal'
import Input2FAModal from './modals/Input2FAModal' import Input2FAModal from './modals/Input2FAModal'
import Reset2FAModal from './modals/Reset2FAModal' import Reset2FAModal from './modals/Reset2FAModal'
@ -26,9 +20,6 @@ import ResetPasswordModal from './modals/ResetPasswordModal'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const url =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const GET_USERS = gql` const GET_USERS = gql`
query users { query users {
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` const CHANGE_USER_ROLE = gql`
mutation changeUserRole($id: ID!, $newRole: String!) { mutation changeUserRole($id: ID!, $newRole: String!) {
changeUserRole(id: $id, newRole: $newRole) { 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 Users = () => {
const classes = useStyles() const classes = useStyles()
@ -74,10 +77,6 @@ const Users = () => {
const { data: userResponse } = useQuery(GET_USERS) const { data: userResponse } = useQuery(GET_USERS)
/* const [deleteUser] = useMutation(DELETE_USERS, {
refetchQueries: () => ['users']
}) */
const [changeUserRole] = useMutation(CHANGE_USER_ROLE, { const [changeUserRole] = useMutation(CHANGE_USER_ROLE, {
refetchQueries: () => ['users'] refetchQueries: () => ['users']
}) })
@ -86,6 +85,22 @@ const Users = () => {
refetchQueries: () => ['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 [userInfo, setUserInfo] = useState(null)
const [showCreateUserModal, setShowCreateUserModal] = useState(false) const [showCreateUserModal, setShowCreateUserModal] = useState(false)
@ -102,81 +117,18 @@ const Users = () => {
const toggleReset2FAModal = () => setShowReset2FAModal(!showReset2FAModal) const toggleReset2FAModal = () => setShowReset2FAModal(!showReset2FAModal)
const [showRoleModal, setShowRoleModal] = useState(false) const [showRoleModal, setShowRoleModal] = useState(false)
const toggleRoleModal = () => const toggleRoleModal = () => setShowRoleModal(!showRoleModal)
setShowRoleModal(!showRoleModal)
const [showEnableUserModal, setShowEnableUserModal] = useState(false) const [showEnableUserModal, setShowEnableUserModal] = useState(false)
const toggleEnableUserModal = () => const toggleEnableUserModal = () =>
setShowEnableUserModal(!showEnableUserModal) setShowEnableUserModal(!showEnableUserModal)
/* const [showDeleteUserModal, setShowDeleteUserModal] = useState(false)
const toggleDeleteUserModal = () =>
setShowDeleteUserModal(!showDeleteUserModal) */
const [showInputConfirmModal, setShowInputConfirmModal] = useState(false) const [showInputConfirmModal, setShowInputConfirmModal] = useState(false)
const toggleInputConfirmModal = () => const toggleInputConfirmModal = () =>
setShowInputConfirmModal(!showInputConfirmModal) setShowInputConfirmModal(!showInputConfirmModal)
const [action, setAction] = useState(null) 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 = [ const elements = [
{ {
header: 'Login', header: 'Login',
@ -248,11 +200,21 @@ const Users = () => {
className={classes.actionChip} className={classes.actionChip}
onClick={() => { onClick={() => {
setUserInfo(u) setUserInfo(u)
if(u.role === 'superuser') { if (u.role === 'superuser') {
setAction(() => requestNewPassword.bind(null, u.id)) setAction(() =>
createResetPasswordToken.bind(null, {
variables: {
userID: u.id
}
})
)
toggleInputConfirmModal() toggleInputConfirmModal()
} else { } else {
requestNewPassword(u.id) createResetPasswordToken({
variables: {
userID: u.id
}
})
} }
}} }}
/> />
@ -262,11 +224,21 @@ const Users = () => {
className={classes.actionChip} className={classes.actionChip}
onClick={() => { onClick={() => {
setUserInfo(u) setUserInfo(u)
if(u.role === 'superuser') { if (u.role === 'superuser') {
setAction(() => requestNew2FA.bind(null, u.id)) setAction(() => () =>
createReset2FAToken({
variables: {
userID: u.id
}
})
)
toggleInputConfirmModal() toggleInputConfirmModal()
} else { } 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', header: 'Enabled',
width: 100, width: 100,
@ -302,22 +262,7 @@ const Users = () => {
value={u.enabled} value={u.enabled}
/> />
) )
}/* , }
{
header: 'Delete',
width: 100,
textAlign: 'center',
size: 'sm',
view: u => (
<IconButton
onClick={() => {
setUserInfo(u)
toggleDeleteUserModal()
}}>
<DeleteIcon />
</IconButton>
)
} */
] ]
return ( return (
@ -366,14 +311,6 @@ const Users = () => {
inputConfirmToggle={toggleInputConfirmModal} inputConfirmToggle={toggleInputConfirmModal}
setAction={setAction} setAction={setAction}
/> />
{/* <DeleteUserModal
showModal={showDeleteUserModal}
toggleModal={toggleDeleteUserModal}
user={userInfo}
confirm={deleteUser}
inputConfirmToggle={toggleInputConfirmModal}
setAction={setAction}
/> */}
<Input2FAModal <Input2FAModal
showModal={showInputConfirmModal} showModal={showInputConfirmModal}
toggleModal={toggleInputConfirmModal} toggleModal={toggleInputConfirmModal}

View file

@ -9,7 +9,9 @@ import {
const styles = { const styles = {
footer: { footer: {
margin: [['auto', 0, spacer * 3, 'auto']] display: 'flex',
flexDirection: 'row',
margin: [['auto', 0, spacer * 3, 0]]
}, },
modalTitle: { modalTitle: {
marginTop: -5, marginTop: -5,
@ -42,9 +44,8 @@ const styles = {
}, },
copyToClipboard: { copyToClipboard: {
marginLeft: 'auto', marginLeft: 'auto',
paddingTop: 6, paddingTop: 7,
paddingLeft: 15, marginRight: -5
marginRight: -11
}, },
chip: { chip: {
backgroundColor: subheaderColor, backgroundColor: subheaderColor,
@ -63,10 +64,12 @@ const styles = {
}, },
addressWrapper: { addressWrapper: {
backgroundColor: subheaderColor, backgroundColor: subheaderColor,
marginTop: 8 marginTop: 8,
height: 35
}, },
address: { address: {
margin: `${spacer * 1.5}px ${spacer * 3}px` margin: `0px ${spacer * 2}px 0px ${spacer * 2}px`,
paddingRight: -15
}, },
errorMessage: { errorMessage: {
fontFamily: fontSecondary, fontFamily: fontSecondary,
@ -75,6 +78,33 @@ const styles = {
codeContainer: { codeContainer: {
marginTop: 15, marginTop: 15,
marginBottom: 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 Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons' 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' import styles from '../UserManagement.styles'
@ -28,18 +28,21 @@ const ChangeRoleModal = ({
{showModal && ( {showModal && (
<Modal <Modal
closeOnBackdropClick={true} closeOnBackdropClick={true}
width={600} width={450}
height={275} height={250}
handleClose={handleClose} handleClose={handleClose}
open={true}> open={true}>
<H2 className={classes.modalTitle}>Change {user.username}'s role?</H2> <Info2 className={classes.modalTitle}>
<Info3 className={classes.info}> Change {user.username}'s role?
</Info2>
<P className={classes.info}>
You are about to alter {user.username}'s role. This will change this You are about to alter {user.username}'s role. This will change this
user's permission to access certain resources. user's permission to access certain resources.
</Info3> </P>
<Info3 className={classes.info}>Do you wish to proceed?</Info3> <P className={classes.info}>Do you wish to proceed?</P>
<div className={classes.footer}> <div className={classes.footer}>
<Button <Button
className={classes.submit}
onClick={() => { onClick={() => {
setAction(() => setAction(() =>
confirm.bind(null, { confirm.bind(null, {
@ -52,7 +55,7 @@ const ChangeRoleModal = ({
inputConfirmToggle() inputConfirmToggle()
handleClose() handleClose()
}}> }}>
Finish Confirm
</Button> </Button>
</div> </div>
</Modal> </Modal>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,9 +6,10 @@ import {
MuiThemeProvider, MuiThemeProvider,
makeStyles makeStyles
} from '@material-ui/core/styles' } from '@material-ui/core/styles'
import { axios } from '@use-hooks/axios'
import { create } from 'jss' import { create } from 'jss'
import extendJss from 'jss-plugin-extend' import extendJss from 'jss-plugin-extend'
import React, { useContext, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { import {
useLocation, useLocation,
useHistory, useHistory,
@ -72,7 +73,7 @@ const Main = () => {
const classes = useStyles() const classes = useStyles()
const location = useLocation() const location = useLocation()
const history = useHistory() const history = useHistory()
const { wizardTested } = useContext(AppContext) const { wizardTested, userData } = useContext(AppContext)
const route = location.pathname const route = location.pathname
@ -91,7 +92,9 @@ const Main = () => {
return ( return (
<div className={classes.root}> <div className={classes.root}>
{!is404 && wizardTested && <Header tree={tree} />} {!is404 && wizardTested && userData && (
<Header tree={tree} user={userData} />
)}
<main className={classes.wrapper}> <main className={classes.wrapper}>
{sidebar && !is404 && wizardTested && ( {sidebar && !is404 && wizardTested && (
<TitleSection title={parent.title}></TitleSection> <TitleSection title={parent.title}></TitleSection>
@ -117,19 +120,47 @@ const Main = () => {
const App = () => { const App = () => {
const [wizardTested, setWizardTested] = useState(false) 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 ( return (
<AppContext.Provider value={{ wizardTested, setWizardTested }}> <AppContext.Provider
<Router> value={{ wizardTested, setWizardTested, userData, setUserData }}>
<ApolloProvider> {!loading && (
<StylesProvider jss={jss}> <Router>
<MuiThemeProvider theme={theme}> <ApolloProvider>
<CssBaseline /> <StylesProvider jss={jss}>
<Main /> <MuiThemeProvider theme={theme}>
</MuiThemeProvider> <CssBaseline />
</StylesProvider> <Main />
</ApolloProvider> </MuiThemeProvider>
</Router> </StylesProvider>
</ApolloProvider>
</Router>
)}
</AppContext.Provider> </AppContext.Provider>
) )
} }

View file

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

View file

@ -7,7 +7,7 @@ import { HttpLink } from 'apollo-link-http'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { useHistory, useLocation } from 'react-router-dom' import { useHistory, useLocation } from 'react-router-dom'
import { AppContext } from 'src/App' 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' : ''

47
package-lock.json generated
View file

@ -24,6 +24,14 @@
"zen-observable": "^0.8.14" "zen-observable": "^0.8.14"
}, },
"dependencies": { "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": { "symbol-observable": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz",
@ -512,6 +520,13 @@
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.10.4" "@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": { "@babel/plugin-syntax-nullish-coalescing-operator": {
@ -538,6 +553,13 @@
"integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.8.0" "@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": { "@babel/plugin-syntax-optional-catch-binding": {
@ -757,6 +779,11 @@
"version": "0.13.7", "version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" "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/code-frame": "^7.10.4",
"@babel/parser": "^7.12.7", "@babel/parser": "^7.12.7",
"@babel/types": "^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": { "@babel/traverse": {
@ -2531,14 +2570,6 @@
"tslib": "^1.14.1" "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": { "@wry/trie": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.2.1.tgz",