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:
parent
fded22f39a
commit
d295acc261
33 changed files with 1319 additions and 1139 deletions
|
|
@ -21,7 +21,9 @@ const options = require('../options')
|
|||
const db = require('../db')
|
||||
const users = require('../users')
|
||||
|
||||
const { typeDefs, resolvers, AuthDirective, SuperuserDirective } = require('./graphql/schema')
|
||||
const authRouter = require('./routes/auth')
|
||||
const { AuthDirective } = require('./graphql/directives')
|
||||
const { typeDefs, resolvers } = require('./graphql/schema')
|
||||
|
||||
const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
|
||||
|
|
@ -64,8 +66,7 @@ const apolloServer = new ApolloServer({
|
|||
typeDefs,
|
||||
resolvers,
|
||||
schemaDirectives: {
|
||||
auth: AuthDirective,
|
||||
superuser: SuperuserDirective
|
||||
auth: AuthDirective
|
||||
},
|
||||
playground: false,
|
||||
introspection: false,
|
||||
|
|
@ -74,7 +75,8 @@ const apolloServer = new ApolloServer({
|
|||
return error
|
||||
},
|
||||
context: async ({ req }) => {
|
||||
if (!req.session.user) throw new AuthenticationError('Authentication failed')
|
||||
if (!req.session.user) return { req }
|
||||
|
||||
const user = await users.verifyAndUpdateUser(
|
||||
req.session.user.id,
|
||||
req.headers['user-agent'] || 'Unknown',
|
||||
|
|
@ -87,7 +89,8 @@ const apolloServer = new ApolloServer({
|
|||
req.session.lastUsed = new Date(Date.now()).toISOString()
|
||||
req.session.user.id = user.id
|
||||
req.session.user.role = user.role
|
||||
return { req: { ...req } }
|
||||
|
||||
return { req }
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -104,9 +107,7 @@ app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' })
|
|||
|
||||
app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false }))
|
||||
app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false }))
|
||||
app.use('/api', register)
|
||||
|
||||
require('./routes/auth')(app)
|
||||
app.use(authRouter)
|
||||
|
||||
// Everything not on graphql or api/register is redirected to the front-end
|
||||
app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))
|
||||
|
|
|
|||
40
lib/new-admin/graphql/directives/auth.js
Normal file
40
lib/new-admin/graphql/directives/auth.js
Normal 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
|
||||
3
lib/new-admin/graphql/directives/index.js
Normal file
3
lib/new-admin/graphql/directives/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const AuthDirective = require('./auth')
|
||||
|
||||
module.exports = { AuthDirective }
|
||||
220
lib/new-admin/graphql/modules/authentication.js
Normal file
220
lib/new-admin/graphql/modules/authentication.js
Normal 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
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ const scalar = require('./scalar.resolver')
|
|||
const settings = require('./settings.resolver')
|
||||
const status = require('./status.resolver')
|
||||
const transaction = require('./transaction.resolver')
|
||||
const user = require('./users.resolver')
|
||||
const version = require('./version.resolver')
|
||||
|
||||
const resolvers = [
|
||||
|
|
@ -35,6 +36,7 @@ const resolvers = [
|
|||
settings,
|
||||
status,
|
||||
transaction,
|
||||
user,
|
||||
version
|
||||
]
|
||||
|
||||
|
|
|
|||
35
lib/new-admin/graphql/resolvers/users.resolver.js
Normal file
35
lib/new-admin/graphql/resolvers/users.resolver.js
Normal 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
|
||||
|
|
@ -16,6 +16,7 @@ const scalar = require('./scalar.type')
|
|||
const settings = require('./settings.type')
|
||||
const status = require('./status.type')
|
||||
const transaction = require('./transaction.type')
|
||||
const user = require('./users.type')
|
||||
const version = require('./version.type')
|
||||
|
||||
const types = [
|
||||
|
|
@ -35,6 +36,7 @@ const types = [
|
|||
settings,
|
||||
status,
|
||||
transaction,
|
||||
user,
|
||||
version
|
||||
]
|
||||
|
||||
|
|
|
|||
77
lib/new-admin/graphql/types/users.type.js
Normal file
77
lib/new-admin/graphql/types/users.type.js
Normal 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
|
||||
|
|
@ -1,91 +1,7 @@
|
|||
const otplib = require('otplib')
|
||||
const bcrypt = require('bcrypt')
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
|
||||
const users = require('../../users')
|
||||
const login = require('../login')
|
||||
|
||||
async function isValidUser (username, password) {
|
||||
const hashedPassword = await login.checkUser(username)
|
||||
if (!hashedPassword) return false
|
||||
|
||||
const isMatch = await bcrypt.compare(password, hashedPassword)
|
||||
if (!isMatch) return false
|
||||
|
||||
const user = await login.validateUser(username, hashedPassword)
|
||||
if (!user) return false
|
||||
return user
|
||||
}
|
||||
|
||||
module.exports = function (app) {
|
||||
app.post('/api/login', function (req, res, next) {
|
||||
const usernameInput = req.body.username
|
||||
const passwordInput = req.body.password
|
||||
|
||||
isValidUser(usernameInput, passwordInput).then(user => {
|
||||
if (!user) return res.sendStatus(403)
|
||||
users.get2FASecret(user.id).then(user => {
|
||||
const twoFASecret = user.twofa_code
|
||||
if (twoFASecret) return res.status(200).json({ message: 'INPUT2FA' })
|
||||
if (!twoFASecret) return res.status(200).json({ message: 'SETUP2FA' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/login/2fa', function (req, res, next) {
|
||||
const code = req.body.twoFACode
|
||||
const username = req.body.username
|
||||
const password = req.body.password
|
||||
const rememberMeInput = req.body.rememberMe
|
||||
|
||||
isValidUser(username, password).then(user => {
|
||||
if (!user) return res.sendStatus(403)
|
||||
|
||||
users.get2FASecret(user.id).then(user => {
|
||||
const secret = user.twofa_code
|
||||
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||
if (!isCodeValid) return res.sendStatus(403)
|
||||
|
||||
const finalUser = { id: user.id, username: user.username, role: user.role }
|
||||
req.session.user = finalUser
|
||||
if (rememberMeInput) req.session.cookie.maxAge = 90 * 24 * 60 * 60 * 1000 // 90 days
|
||||
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/login/2fa/setup', function (req, res, next) {
|
||||
const username = req.body.username
|
||||
const password = req.body.password
|
||||
|
||||
// TODO: maybe check if the user already has a 2fa secret
|
||||
isValidUser(username, password).then(user => {
|
||||
if (!user) return res.sendStatus(403)
|
||||
|
||||
const secret = otplib.authenticator.generateSecret()
|
||||
const otpauth = otplib.authenticator.keyuri(username, 'Lamassu Industries', secret)
|
||||
return res.status(200).json({ secret, otpauth })
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/login/2fa/save', function (req, res, next) {
|
||||
const username = req.body.username
|
||||
const password = req.body.password
|
||||
const secret = req.body.secret
|
||||
const code = req.body.code
|
||||
|
||||
isValidUser(username, password).then(user => {
|
||||
if (!user || !secret) return res.sendStatus(403)
|
||||
|
||||
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||
if (!isCodeValid) return res.sendStatus(403)
|
||||
|
||||
users.save2FASecret(user.id, secret)
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/user-data', function (req, res, next) {
|
||||
const getUserData = function (req, res, next) {
|
||||
const lidCookie = req.cookies && req.cookies.lid
|
||||
if (!lidCookie) {
|
||||
res.sendStatus(403)
|
||||
|
|
@ -94,166 +10,8 @@ module.exports = function (app) {
|
|||
|
||||
const user = req.session.user
|
||||
return res.status(200).json({ message: 'Success', user: user })
|
||||
})
|
||||
|
||||
app.post('/api/resetpassword', function (req, res, next) {
|
||||
const userID = req.body.userID
|
||||
|
||||
users.findById(userID)
|
||||
.then(user => {
|
||||
if (!user) return res.sendStatus(403)
|
||||
return users.createResetPasswordToken(user.id)
|
||||
})
|
||||
.then(token => {
|
||||
return res.status(200).json({ token })
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
})
|
||||
|
||||
app.get('/api/resetpassword', function (req, res, next) {
|
||||
const token = req.query.t
|
||||
|
||||
if (!token) return res.sendStatus(400)
|
||||
return users.validatePasswordResetToken(token)
|
||||
.then(r => {
|
||||
if (!r.success) return res.status(200).send('The link has expired')
|
||||
return res.status(200).json({ userID: r.userID })
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
res.sendStatus(400)
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/updatepassword', function (req, res, next) {
|
||||
const userID = req.body.userID
|
||||
const newPassword = req.body.newPassword
|
||||
|
||||
users.findById(userID).then(user => {
|
||||
if (req.session.user && user.id === req.session.user.id) req.session.destroy()
|
||||
return users.updatePassword(user.id, newPassword)
|
||||
}).then(() => {
|
||||
res.sendStatus(200)
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
res.sendStatus(400)
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/reset2fa', function (req, res, next) {
|
||||
const userID = req.body.userID
|
||||
|
||||
users.findById(userID)
|
||||
.then(user => {
|
||||
if (!user) return res.sendStatus(403)
|
||||
return users.createReset2FAToken(user.id)
|
||||
})
|
||||
.then(token => {
|
||||
return res.status(200).json({ token })
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
})
|
||||
|
||||
app.get('/api/reset2fa', function (req, res, next) {
|
||||
const token = req.query.t
|
||||
|
||||
if (!token) return res.sendStatus(400)
|
||||
return users.validate2FAResetToken(token)
|
||||
.then(r => {
|
||||
if (!r.success) return res.status(200).send('The link has expired')
|
||||
return users.findById(r.userID)
|
||||
})
|
||||
.then(user => {
|
||||
const secret = otplib.authenticator.generateSecret()
|
||||
const otpauth = otplib.authenticator.keyuri(user.username, 'Lamassu Industries', secret)
|
||||
return res.status(200).json({ userID: user.id, secret, otpauth })
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
res.sendStatus(400)
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/update2fa', function (req, res, next) {
|
||||
const userID = req.body.userID
|
||||
const secret = req.body.secret
|
||||
const code = req.body.code
|
||||
|
||||
users.findById(userID).then(user => {
|
||||
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||
if (!isCodeValid) return res.sendStatus(401)
|
||||
|
||||
if (req.session.user && user.id === req.session.user.id) req.session.destroy()
|
||||
users.save2FASecret(user.id, secret).then(() => { return res.sendStatus(200) })
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
return res.sendStatus(400)
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/createuser', function (req, res, next) {
|
||||
const username = req.body.username
|
||||
const role = req.body.role
|
||||
|
||||
users.getByName(username)
|
||||
.then(user => {
|
||||
if (user) return res.status(200).json({ message: 'User already exists!' })
|
||||
|
||||
users.createUserRegistrationToken(username, role).then(token => {
|
||||
return res.status(200).json({ token })
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
res.sendStatus(400)
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/register', function (req, res, next) {
|
||||
const token = req.query.t
|
||||
|
||||
if (!token) return res.sendStatus(400)
|
||||
users.validateUserRegistrationToken(token)
|
||||
.then(r => {
|
||||
if (!r.success) return res.status(200).json({ message: 'The link has expired' })
|
||||
return res.status(200).json({ username: r.username, role: r.role })
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
res.sendStatus(400)
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/register', function (req, res, next) {
|
||||
const username = req.body.username
|
||||
const password = req.body.password
|
||||
const role = req.body.role
|
||||
|
||||
users.getByName(username)
|
||||
.then(user => {
|
||||
if (user) return res.status(200).json({ message: 'User already exists!' })
|
||||
|
||||
users.createUser(username, password, role)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
res.sendStatus(400)
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/confirm2fa', function (req, res, next) {
|
||||
const code = req.body.code
|
||||
const requestingUser = req.session.user
|
||||
|
||||
if (!requestingUser) return res.status(403)
|
||||
|
||||
users.get2FASecret(requestingUser.id).then(user => {
|
||||
const secret = user.twofa_code
|
||||
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||
if (!isCodeValid) return res.sendStatus(401)
|
||||
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
router.get('/user-data', getUserData)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
import { axios } from '@use-hooks/axios'
|
||||
import { create } from 'jss'
|
||||
import extendJss from 'jss-plugin-extend'
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
useLocation,
|
||||
useHistory,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
|
|
@ -10,11 +11,34 @@ import { H2, P } from 'src/components/typography'
|
|||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const INPUT_2FA = gql`
|
||||
mutation input2FA(
|
||||
$username: String!
|
||||
$password: String!
|
||||
$code: String!
|
||||
$rememberMe: Boolean!
|
||||
) {
|
||||
input2FA(
|
||||
username: $username
|
||||
password: $password
|
||||
code: $code
|
||||
rememberMe: $rememberMe
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_USER_DATA = gql`
|
||||
{
|
||||
userData {
|
||||
id
|
||||
username
|
||||
role
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Input2FAState = ({
|
||||
twoFAField,
|
||||
onTwoFAChange,
|
||||
|
|
@ -33,53 +57,25 @@ const Input2FAState = ({
|
|||
setInvalidToken(false)
|
||||
}
|
||||
|
||||
const handle2FA = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login/2fa`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
rememberMe: rememberMeField,
|
||||
twoFACode: twoFAField
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
const [input2FA, { error: mutationError }] = useMutation(INPUT_2FA, {
|
||||
onCompleted: ({ input2FA: success }) => {
|
||||
success ? getUserData() : setInvalidToken(true)
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
getUserData()
|
||||
|
||||
const [getUserData, { error: queryError }] = useLazyQuery(GET_USER_DATA, {
|
||||
onCompleted: ({ userData }) => {
|
||||
setUserData(userData)
|
||||
history.push('/')
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) {
|
||||
onTwoFAChange('')
|
||||
setInvalidToken(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getUserData = () => {
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: `${url}/user-data`,
|
||||
withCredentials: true
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 200) setUserData(res.data.user)
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.status === 403) setUserData(null)
|
||||
})
|
||||
const getErrorMsg = () => {
|
||||
if (mutationError || queryError) return 'Internal server error'
|
||||
if (twoFAField.length !== 6 && invalidToken)
|
||||
return 'The code should have 6 characters!'
|
||||
if (invalidToken) return 'Code is invalid. Please try again.'
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -93,16 +89,26 @@ const Input2FAState = ({
|
|||
onChange={handle2FAChange}
|
||||
numInputs={6}
|
||||
error={invalidToken}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
<div className={classes.twofaFooter}>
|
||||
{invalidToken && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handle2FA()
|
||||
if (twoFAField.length !== 6) {
|
||||
setInvalidToken(true)
|
||||
return
|
||||
}
|
||||
input2FA({
|
||||
variables: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
code: twoFAField,
|
||||
rememberMe: rememberMeField
|
||||
}
|
||||
})
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Login
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ const Login = () => {
|
|||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<LoginCard />
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const styles = {
|
|||
marginBottom: 30
|
||||
},
|
||||
rememberMeWrapper: {
|
||||
marginTop: 35,
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
|
@ -61,15 +62,14 @@ const styles = {
|
|||
background: 'url(/wizard-background.svg) no-repeat center center fixed',
|
||||
backgroundColor: backgroundColor,
|
||||
backgroundSize: 'cover',
|
||||
// filter: 'blur(4px)',
|
||||
// pointerEvents: 'none',
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
position: 'relative',
|
||||
left: '50%',
|
||||
right: '50%',
|
||||
marginLeft: '-50vw',
|
||||
marginRight: '-50vw'
|
||||
marginRight: '-50vw',
|
||||
minHeight: '100vh'
|
||||
},
|
||||
info: {
|
||||
fontFamily: fontSecondary,
|
||||
|
|
@ -115,6 +115,12 @@ const styles = {
|
|||
},
|
||||
confirm2FAInput: {
|
||||
marginTop: 25
|
||||
},
|
||||
confirmPassword: {
|
||||
marginTop: 25
|
||||
},
|
||||
error: {
|
||||
color: errorColor
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,11 +52,8 @@ const LoginCard = () => {
|
|||
case STATES.LOGIN:
|
||||
return (
|
||||
<LoginState
|
||||
clientField={clientField}
|
||||
onClientChange={onClientChange}
|
||||
passwordField={passwordField}
|
||||
onPasswordChange={onPasswordChange}
|
||||
rememberMeField={rememberMeField}
|
||||
onRememberMeChange={onRememberMeChange}
|
||||
STATES={STATES}
|
||||
handleLoginState={handleLoginState}
|
||||
|
|
|
|||
|
|
@ -1,128 +1,134 @@
|
|||
import { useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { Checkbox, TextInput } from 'src/components/inputs/base'
|
||||
import { Checkbox, SecretInput, TextInput } from 'src/components/inputs/formik'
|
||||
import { Label2, P } from 'src/components/typography'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const LOGIN = gql`
|
||||
mutation login($username: String!, $password: String!) {
|
||||
login(username: $username, password: $password)
|
||||
}
|
||||
`
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
client: Yup.string()
|
||||
.required('Client field is required!')
|
||||
.email('Username field should be in an email format!'),
|
||||
password: Yup.string().required('Password field is required'),
|
||||
rememberMe: Yup.boolean()
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
client: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
}
|
||||
|
||||
const LoginState = ({
|
||||
clientField,
|
||||
onClientChange,
|
||||
passwordField,
|
||||
onPasswordChange,
|
||||
rememberMeField,
|
||||
onRememberMeChange,
|
||||
STATES,
|
||||
handleLoginState
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [login, { error: mutationError }] = useMutation(LOGIN, {
|
||||
onCompleted: ({ login }) => {
|
||||
if (login === 'INPUT2FA') handleLoginState(STATES.INPUT_2FA)
|
||||
if (login === 'SETUP2FA') handleLoginState(STATES.SETUP_2FA)
|
||||
if (login === 'FAILED') setInvalidLogin(true)
|
||||
}
|
||||
})
|
||||
|
||||
const [invalidLogin, setInvalidLogin] = useState(false)
|
||||
|
||||
const handleClientChange = event => {
|
||||
onClientChange(event.target.value)
|
||||
setInvalidLogin(false)
|
||||
}
|
||||
|
||||
const handlePasswordChange = event => {
|
||||
onPasswordChange(event.target.value)
|
||||
setInvalidLogin(false)
|
||||
}
|
||||
|
||||
const handleRememberMeChange = () => {
|
||||
onRememberMeChange(!rememberMeField)
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
rememberMe: rememberMeField
|
||||
},
|
||||
options: {
|
||||
withCredentials: true
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
const message = res.data.message
|
||||
if (status === 200 && message === 'INPUT2FA')
|
||||
handleLoginState(STATES.INPUT_2FA)
|
||||
if (status === 200 && message === 'SETUP2FA')
|
||||
handleLoginState(STATES.SETUP_2FA)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) setInvalidLogin(true)
|
||||
}
|
||||
})
|
||||
const getErrorMsg = (formikErrors, formikTouched) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (mutationError) return 'Internal server error'
|
||||
if (formikErrors.client && formikTouched.client) return formikErrors.client
|
||||
if (formikErrors.password && formikTouched.password)
|
||||
return formikErrors.password
|
||||
if (invalidLogin) return 'Invalid login/password combination'
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>Client</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidLogin}
|
||||
name="client-name"
|
||||
<Formik
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={values => {
|
||||
setInvalidLogin(false)
|
||||
onClientChange(values.client)
|
||||
onPasswordChange(values.password)
|
||||
onRememberMeChange(values.rememberMe)
|
||||
login({
|
||||
variables: {
|
||||
username: values.client,
|
||||
password: values.password
|
||||
}
|
||||
})
|
||||
}}>
|
||||
{({ errors, touched }) => (
|
||||
<Form id="login-form">
|
||||
<Field
|
||||
name="client"
|
||||
label="Client"
|
||||
size="lg"
|
||||
component={TextInput}
|
||||
fullWidth
|
||||
autoFocus
|
||||
id="client-name"
|
||||
type="text"
|
||||
size="lg"
|
||||
onChange={handleClientChange}
|
||||
value={clientField}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>Password</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidLogin}
|
||||
error={getErrorMsg(errors, touched)}
|
||||
onKeyUp={() => {
|
||||
if (invalidLogin) setInvalidLogin(false)
|
||||
}}
|
||||
/>
|
||||
<Field
|
||||
name="password"
|
||||
id="password"
|
||||
type="password"
|
||||
size="lg"
|
||||
onChange={handlePasswordChange}
|
||||
value={passwordField}
|
||||
component={SecretInput}
|
||||
label="Password"
|
||||
fullWidth
|
||||
error={getErrorMsg(errors, touched)}
|
||||
onKeyUp={() => {
|
||||
if (invalidLogin) setInvalidLogin(false)
|
||||
}}
|
||||
/>
|
||||
<div className={classes.rememberMeWrapper}>
|
||||
<Checkbox
|
||||
<Field
|
||||
name="rememberMe"
|
||||
className={classes.checkbox}
|
||||
id="remember-me"
|
||||
onChange={handleRememberMeChange}
|
||||
value={rememberMeField}
|
||||
component={Checkbox}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>Keep me logged in</Label2>
|
||||
</div>
|
||||
<div className={classes.footer}>
|
||||
{invalidLogin && (
|
||||
{getErrorMsg(errors, touched) && (
|
||||
<P className={classes.errorMessage}>
|
||||
Invalid login/password combination.
|
||||
{getErrorMsg(errors, touched)}
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleLogin()
|
||||
}}
|
||||
type="submit"
|
||||
form="login-form"
|
||||
buttonClassName={classes.loginButton}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +1,97 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import axios from 'axios'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState } from 'react'
|
||||
import { useLocation, useHistory } from 'react-router-dom'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/base'
|
||||
import { SecretInput } from 'src/components/inputs/formik'
|
||||
import { H2, Label2, P } from 'src/components/typography'
|
||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||
const QueryParams = () => new URLSearchParams(useLocation().search)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
const VALIDATE_REGISTER_LINK = gql`
|
||||
query validateRegisterLink($token: String!) {
|
||||
validateRegisterLink(token: $token) {
|
||||
username
|
||||
role
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const REGISTER = gql`
|
||||
mutation register($username: String!, $password: String!, $role: String!) {
|
||||
register(username: $username, password: $password, role: $role)
|
||||
}
|
||||
`
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.required('A password is required')
|
||||
.test(
|
||||
'len',
|
||||
'Your password must contain more than 8 characters',
|
||||
val => val.length >= 8
|
||||
),
|
||||
confirmPassword: Yup.string().oneOf(
|
||||
[Yup.ref('password'), null],
|
||||
'Passwords must match'
|
||||
)
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
|
||||
const Register = () => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const query = useQuery()
|
||||
const [passwordField, setPasswordField] = useState('')
|
||||
const [confirmPasswordField, setConfirmPasswordField] = useState('')
|
||||
const [invalidPassword, setInvalidPassword] = useState(false)
|
||||
const token = QueryParams().get('t')
|
||||
const [username, setUsername] = useState(null)
|
||||
const [role, setRole] = useState(null)
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [wasSuccessful, setSuccess] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
validateQuery()
|
||||
}, [])
|
||||
|
||||
const validateQuery = () => {
|
||||
axios({
|
||||
url: `${url}/api/register?t=${query.get('t')}`,
|
||||
method: 'GET',
|
||||
options: {
|
||||
withCredentials: true
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
const { error: queryError } = useQuery(VALIDATE_REGISTER_LINK, {
|
||||
variables: { token: token },
|
||||
onCompleted: ({ validateRegisterLink: info }) => {
|
||||
setLoading(false)
|
||||
if (res.data === 'The link has expired') setSuccess(false)
|
||||
else {
|
||||
if (!info) {
|
||||
setSuccess(false)
|
||||
} else {
|
||||
setSuccess(true)
|
||||
setUsername(res.data.username)
|
||||
setRole(res.data.role)
|
||||
setUsername(info.username)
|
||||
setRole(info.role)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
if (!isValidPassword()) return setInvalidPassword(true)
|
||||
axios({
|
||||
url: `${url}/api/register`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
username: username,
|
||||
password: passwordField,
|
||||
role: role
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
onError: () => {
|
||||
setLoading(false)
|
||||
setSuccess(false)
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
history.push('/wizard', { fromAuthRegister: true })
|
||||
|
||||
const [register, { error: mutationError }] = useMutation(REGISTER, {
|
||||
onCompleted: ({ register: success }) => {
|
||||
if (success) history.push('/wizard', { fromAuthRegister: true })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const isValidPassword = () => {
|
||||
return passwordField === confirmPasswordField
|
||||
}
|
||||
|
||||
const handlePasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setPasswordField(event.target.value)
|
||||
}
|
||||
|
||||
const handleConfirmPasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setConfirmPasswordField(event.target.value)
|
||||
const getErrorMsg = (formikErrors, formikTouched) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (queryError || mutationError) return 'Internal server error'
|
||||
if (formikErrors.password && formikTouched.password)
|
||||
return formikErrors.password
|
||||
if (formikErrors.confirmPassword && formikTouched.confirmPassword)
|
||||
return formikErrors.confirmPassword
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -107,7 +101,6 @@ const Register = () => {
|
|||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<div>
|
||||
|
|
@ -118,49 +111,52 @@ const Register = () => {
|
|||
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||
</div>
|
||||
{!isLoading && wasSuccessful && (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Insert a password
|
||||
</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="new-password"
|
||||
<Formik
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={values => {
|
||||
register({
|
||||
variables: {
|
||||
username: username,
|
||||
password: values.password,
|
||||
role: role
|
||||
}
|
||||
})
|
||||
}}>
|
||||
{({ errors, touched }) => (
|
||||
<Form id="register-form">
|
||||
<Field
|
||||
name="password"
|
||||
label="Insert a password"
|
||||
autoFocus
|
||||
id="new-password"
|
||||
type="password"
|
||||
component={SecretInput}
|
||||
size="lg"
|
||||
onChange={handlePasswordChange}
|
||||
value={passwordField}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Confirm password
|
||||
</Label2>
|
||||
<TextInput
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="confirm-password"
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
name="confirmPassword"
|
||||
label="Confirm your password"
|
||||
component={SecretInput}
|
||||
size="lg"
|
||||
onChange={handleConfirmPasswordChange}
|
||||
value={confirmPasswordField}
|
||||
fullWidth
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{invalidPassword && (
|
||||
{getErrorMsg(errors, touched) && (
|
||||
<P className={classes.errorMessage}>
|
||||
Passwords do not match!
|
||||
{getErrorMsg(errors, touched)}
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleRegister()
|
||||
}}
|
||||
type="submit"
|
||||
form="register-form"
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
{!isLoading && !wasSuccessful && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import axios from 'axios'
|
||||
import gql from 'graphql-tag'
|
||||
import QRCode from 'qrcode.react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useLocation, useHistory } from 'react-router-dom'
|
||||
|
||||
import { ActionButton, Button } from 'src/components/buttons'
|
||||
|
|
@ -13,16 +14,29 @@ import { primaryColor } from 'src/styling/variables'
|
|||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||
const QueryParams = () => new URLSearchParams(useLocation().search)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
const VALIDATE_RESET_2FA_LINK = gql`
|
||||
query validateReset2FALink($token: String!) {
|
||||
validateReset2FALink(token: $token) {
|
||||
user_id
|
||||
secret
|
||||
otpauth
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const RESET_2FA = gql`
|
||||
mutation reset2FA($userID: ID!, $secret: String!, $code: String!) {
|
||||
reset2FA(userID: $userID, secret: $secret, code: $code)
|
||||
}
|
||||
`
|
||||
|
||||
const Reset2FA = () => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const query = useQuery()
|
||||
const token = QueryParams().get('t')
|
||||
const [userID, setUserID] = useState(null)
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [wasSuccessful, setSuccess] = useState(false)
|
||||
|
|
@ -38,61 +52,37 @@ const Reset2FA = () => {
|
|||
setInvalidToken(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
validateQuery()
|
||||
}, [])
|
||||
|
||||
const validateQuery = () => {
|
||||
axios({
|
||||
url: `${url}/api/reset2fa?t=${query.get('t')}`,
|
||||
method: 'GET',
|
||||
options: {
|
||||
withCredentials: true
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
const { error: queryError } = useQuery(VALIDATE_RESET_2FA_LINK, {
|
||||
variables: { token: token },
|
||||
onCompleted: ({ validateReset2FALink: info }) => {
|
||||
setLoading(false)
|
||||
if (res.data === 'The link has expired') setSuccess(false)
|
||||
else {
|
||||
setUserID(res.data.userID)
|
||||
setSecret(res.data.secret)
|
||||
setOtpauth(res.data.otpauth)
|
||||
if (!info) {
|
||||
setSuccess(false)
|
||||
} else {
|
||||
setUserID(info.user_id)
|
||||
setSecret(info.secret)
|
||||
setOtpauth(info.otpauth)
|
||||
setSuccess(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const handle2FAReset = () => {
|
||||
axios({
|
||||
url: `${url}/api/update2fa`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
userID: userID,
|
||||
secret: secret,
|
||||
code: twoFAConfirmation
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
onError: () => {
|
||||
setLoading(false)
|
||||
setSuccess(false)
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
history.push('/')
|
||||
|
||||
const [reset2FA, { error: mutationError }] = useMutation(RESET_2FA, {
|
||||
onCompleted: ({ reset2FA: success }) => {
|
||||
success ? history.push('/') : setInvalidToken(true)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
setInvalidToken(true)
|
||||
})
|
||||
|
||||
const getErrorMsg = () => {
|
||||
if (mutationError || queryError) return 'Internal server error'
|
||||
if (twoFAConfirmation.length !== 6 && invalidToken)
|
||||
return 'The code should have 6 characters!'
|
||||
if (invalidToken) return 'Code is invalid. Please try again.'
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -102,7 +92,6 @@ const Reset2FA = () => {
|
|||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<div>
|
||||
|
|
@ -118,8 +107,7 @@ const Reset2FA = () => {
|
|||
<Label2 className={classes.info2}>
|
||||
To finish this process, please scan the following QR code
|
||||
or insert the secret further below on an authentication
|
||||
app of your choice, preferably Google Authenticator or
|
||||
Authy.
|
||||
app of your choice, such Google Authenticator or Authy.
|
||||
</Label2>
|
||||
</div>
|
||||
<div className={classes.qrCodeWrapper}>
|
||||
|
|
@ -150,17 +138,26 @@ const Reset2FA = () => {
|
|||
onChange={handle2FAChange}
|
||||
numInputs={6}
|
||||
error={invalidToken}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.twofaFooter}>
|
||||
{invalidToken && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handle2FAReset()
|
||||
if (twoFAConfirmation.length !== 6) {
|
||||
setInvalidToken(true)
|
||||
return
|
||||
}
|
||||
reset2FA({
|
||||
variables: {
|
||||
userID: userID,
|
||||
secret: secret,
|
||||
code: twoFAConfirmation
|
||||
}
|
||||
})
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
|
|
|
|||
|
|
@ -1,100 +1,94 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import axios from 'axios'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState } from 'react'
|
||||
import { useLocation, useHistory } from 'react-router-dom'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/base'
|
||||
import { SecretInput } from 'src/components/inputs/formik/'
|
||||
import { H2, Label2, P } from 'src/components/typography'
|
||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||
const QueryParams = () => new URLSearchParams(useLocation().search)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
const VALIDATE_RESET_PASSWORD_LINK = gql`
|
||||
query validateResetPasswordLink($token: String!) {
|
||||
validateResetPasswordLink(token: $token) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const RESET_PASSWORD = gql`
|
||||
mutation resetPassword($userID: ID!, $newPassword: String!) {
|
||||
resetPassword(userID: $userID, newPassword: $newPassword)
|
||||
}
|
||||
`
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.required('A new password is required')
|
||||
.test(
|
||||
'len',
|
||||
'New password must contain more than 8 characters',
|
||||
val => val.length >= 8
|
||||
),
|
||||
confirmPassword: Yup.string().oneOf(
|
||||
[Yup.ref('password'), null],
|
||||
'Passwords must match'
|
||||
)
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
|
||||
const ResetPassword = () => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const query = useQuery()
|
||||
const [newPasswordField, setNewPasswordField] = useState('')
|
||||
const [confirmPasswordField, setConfirmPasswordField] = useState('')
|
||||
const [invalidPassword, setInvalidPassword] = useState(false)
|
||||
const token = QueryParams().get('t')
|
||||
const [userID, setUserID] = useState(null)
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [wasSuccessful, setSuccess] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
validateQuery()
|
||||
}, [])
|
||||
|
||||
const validateQuery = () => {
|
||||
axios({
|
||||
url: `${url}/api/resetpassword?t=${query.get('t')}`,
|
||||
method: 'GET',
|
||||
options: {
|
||||
withCredentials: true
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
useQuery(VALIDATE_RESET_PASSWORD_LINK, {
|
||||
variables: { token: token },
|
||||
onCompleted: ({ validateResetPasswordLink: info }) => {
|
||||
setLoading(false)
|
||||
if (res.data === 'The link has expired') setSuccess(false)
|
||||
else {
|
||||
if (!info) {
|
||||
setSuccess(false)
|
||||
} else {
|
||||
setSuccess(true)
|
||||
setUserID(res.data.userID)
|
||||
setUserID(info.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const handlePasswordReset = () => {
|
||||
if (!isValidPasswordChange()) return setInvalidPassword(true)
|
||||
axios({
|
||||
url: `${url}/api/updatepassword`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
userID: userID,
|
||||
newPassword: newPasswordField
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
onError: () => {
|
||||
setLoading(false)
|
||||
setSuccess(false)
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
history.push('/')
|
||||
|
||||
const [resetPassword, { error }] = useMutation(RESET_PASSWORD, {
|
||||
onCompleted: ({ resetPassword: success }) => {
|
||||
if (success) history.push('/')
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const isValidPasswordChange = () => {
|
||||
return newPasswordField === confirmPasswordField
|
||||
}
|
||||
|
||||
const handleNewPasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setNewPasswordField(event.target.value)
|
||||
}
|
||||
|
||||
const handleConfirmPasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setConfirmPasswordField(event.target.value)
|
||||
const getErrorMsg = (formikErrors, formikTouched) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (error) return 'Internal server error'
|
||||
if (formikErrors.password && formikTouched.password)
|
||||
return formikErrors.password
|
||||
if (formikErrors.confirmPassword && formikTouched.confirmPassword)
|
||||
return formikErrors.confirmPassword
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -104,7 +98,6 @@ const ResetPassword = () => {
|
|||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<div>
|
||||
|
|
@ -115,49 +108,51 @@ const ResetPassword = () => {
|
|||
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||
</div>
|
||||
{!isLoading && wasSuccessful && (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Insert new password
|
||||
</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="new-password"
|
||||
<Formik
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={values => {
|
||||
resetPassword({
|
||||
variables: {
|
||||
userID: userID,
|
||||
newPassword: values.confirmPassword
|
||||
}
|
||||
})
|
||||
}}>
|
||||
{({ errors, touched }) => (
|
||||
<Form id="reset-password">
|
||||
<Field
|
||||
name="password"
|
||||
autoFocus
|
||||
id="new-password"
|
||||
type="password"
|
||||
size="lg"
|
||||
onChange={handleNewPasswordChange}
|
||||
value={newPasswordField}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Confirm new password
|
||||
</Label2>
|
||||
<TextInput
|
||||
component={SecretInput}
|
||||
label="New password"
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="confirm-password"
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
name="confirmPassword"
|
||||
size="lg"
|
||||
onChange={handleConfirmPasswordChange}
|
||||
value={confirmPasswordField}
|
||||
component={SecretInput}
|
||||
label="Confirm your password"
|
||||
fullWidth
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{invalidPassword && (
|
||||
{getErrorMsg(errors, touched) && (
|
||||
<P className={classes.errorMessage}>
|
||||
Passwords do not match!
|
||||
{getErrorMsg(errors, touched)}
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePasswordReset()
|
||||
}}
|
||||
type="submit"
|
||||
form="reset-password"
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
{!isLoading && !wasSuccessful && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useMutation, useQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import gql from 'graphql-tag'
|
||||
import QRCode from 'qrcode.react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { ActionButton, Button } from 'src/components/buttons'
|
||||
import { CodeInput } from 'src/components/inputs/base'
|
||||
|
|
@ -10,8 +11,30 @@ import { primaryColor } from 'src/styling/variables'
|
|||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
const SETUP_2FA = gql`
|
||||
mutation setup2FA(
|
||||
$username: String!
|
||||
$password: String!
|
||||
$secret: String!
|
||||
$codeConfirmation: String!
|
||||
) {
|
||||
setup2FA(
|
||||
username: $username
|
||||
password: $password
|
||||
secret: $secret
|
||||
codeConfirmation: $codeConfirmation
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_2FA_SECRET = gql`
|
||||
query get2FASecret($username: String!, $password: String!) {
|
||||
get2FASecret(username: $username, password: $password) {
|
||||
secret
|
||||
otpauth
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
|
@ -35,72 +58,26 @@ const Setup2FAState = ({
|
|||
setInvalidToken(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
get2FASecret()
|
||||
}, [])
|
||||
const { error: queryError } = useQuery(GET_2FA_SECRET, {
|
||||
variables: { username: clientField, password: passwordField },
|
||||
onCompleted: ({ get2FASecret }) => {
|
||||
setSecret(get2FASecret.secret)
|
||||
setOtpauth(get2FASecret.otpauth)
|
||||
}
|
||||
})
|
||||
|
||||
const get2FASecret = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login/2fa/setup`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField
|
||||
},
|
||||
options: {
|
||||
withCredentials: true
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
const [setup2FA, { error: mutationError }] = useMutation(SETUP_2FA, {
|
||||
onCompleted: ({ setup2FA: success }) => {
|
||||
success ? handleLoginState(STATES.LOGIN) : setInvalidToken(true)
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
setSecret(res.data.secret)
|
||||
setOtpauth(res.data.otpauth)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) {
|
||||
handleLoginState(STATES.LOGIN)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const save2FASecret = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login/2fa/save`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
secret: secret,
|
||||
code: twoFAConfirmation
|
||||
},
|
||||
options: {
|
||||
withCredentials: true
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) console.log(err)
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) handleLoginState(STATES.LOGIN)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) {
|
||||
setInvalidToken(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
const getErrorMsg = () => {
|
||||
if (mutationError || queryError) return 'Internal server error'
|
||||
if (twoFAConfirmation.length !== 6 && invalidToken)
|
||||
return 'The code should have 6 characters!'
|
||||
if (invalidToken) return 'Code is invalid. Please try again.'
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -116,7 +93,7 @@ const Setup2FAState = ({
|
|||
<Label2 className={classes.info2}>
|
||||
To finish this process, please scan the following QR code or
|
||||
insert the secret further below on an authentication app of your
|
||||
choice, preferably Google Authenticator or Authy.
|
||||
choice, such as Google Authenticator or Authy.
|
||||
</Label2>
|
||||
</div>
|
||||
<div className={classes.qrCodeWrapper}>
|
||||
|
|
@ -144,17 +121,27 @@ const Setup2FAState = ({
|
|||
onChange={handle2FAChange}
|
||||
numInputs={6}
|
||||
error={invalidToken}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.twofaFooter}>
|
||||
{invalidToken && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
save2FASecret()
|
||||
if (twoFAConfirmation.length !== 6) {
|
||||
setInvalidToken(true)
|
||||
return
|
||||
}
|
||||
setup2FA({
|
||||
variables: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
secret: secret,
|
||||
codeConfirmation: twoFAConfirmation
|
||||
}
|
||||
})
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
|
|
@ -162,16 +149,7 @@ const Setup2FAState = ({
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
// TODO: should maybe show a spinner here?
|
||||
<div className={classes.twofaFooter}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log('response should be arriving soon')
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Generate Two Factor Authentication Secret
|
||||
</Button>
|
||||
</div>
|
||||
<div></div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const CopyToClipboard = ({
|
|||
className,
|
||||
buttonClassname,
|
||||
children,
|
||||
wrapperClassname,
|
||||
...props
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
|
|
@ -38,7 +39,7 @@ const CopyToClipboard = ({
|
|||
const id = open ? 'simple-popper' : undefined
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classnames(classes.wrapper, wrapperClassname)}>
|
||||
{children && (
|
||||
<>
|
||||
<div className={classnames(classes.address, className)}>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
/* eslint-disable prettier/prettier */
|
||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Box, Chip } from '@material-ui/core'
|
||||
import axios from 'axios'
|
||||
import gql from 'graphql-tag'
|
||||
// import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useContext } from 'react'
|
||||
// import parser from 'ua-parser-js'
|
||||
|
||||
import { AppContext } from 'src/App'
|
||||
import { Link /*, IconButton */ } from 'src/components/buttons'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
// import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
import styles from './UserManagement.styles'
|
||||
import ChangeRoleModal from './modals/ChangeRoleModal'
|
||||
import CreateUserModal from './modals/CreateUserModal'
|
||||
// import DeleteUserModal from './modals/DeleteUserModal'
|
||||
import EnableUserModal from './modals/EnableUserModal'
|
||||
import Input2FAModal from './modals/Input2FAModal'
|
||||
import Reset2FAModal from './modals/Reset2FAModal'
|
||||
|
|
@ -26,9 +20,6 @@ import ResetPasswordModal from './modals/ResetPasswordModal'
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const GET_USERS = gql`
|
||||
query users {
|
||||
users {
|
||||
|
|
@ -43,14 +34,6 @@ const GET_USERS = gql`
|
|||
}
|
||||
`
|
||||
|
||||
/* const DELETE_USERS = gql`
|
||||
mutation deleteUser($id: ID!) {
|
||||
deleteUser(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
` */
|
||||
|
||||
const CHANGE_USER_ROLE = gql`
|
||||
mutation changeUserRole($id: ID!, $newRole: String!) {
|
||||
changeUserRole(id: $id, newRole: $newRole) {
|
||||
|
|
@ -67,6 +50,26 @@ const TOGGLE_USER_ENABLE = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const CREATE_RESET_PASSWORD_TOKEN = gql`
|
||||
mutation createResetPasswordToken($userID: ID!) {
|
||||
createResetPasswordToken(userID: $userID) {
|
||||
token
|
||||
user_id
|
||||
expire
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CREATE_RESET_2FA_TOKEN = gql`
|
||||
mutation createReset2FAToken($userID: ID!) {
|
||||
createReset2FAToken(userID: $userID) {
|
||||
token
|
||||
user_id
|
||||
expire
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Users = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -74,10 +77,6 @@ const Users = () => {
|
|||
|
||||
const { data: userResponse } = useQuery(GET_USERS)
|
||||
|
||||
/* const [deleteUser] = useMutation(DELETE_USERS, {
|
||||
refetchQueries: () => ['users']
|
||||
}) */
|
||||
|
||||
const [changeUserRole] = useMutation(CHANGE_USER_ROLE, {
|
||||
refetchQueries: () => ['users']
|
||||
})
|
||||
|
|
@ -86,6 +85,22 @@ const Users = () => {
|
|||
refetchQueries: () => ['users']
|
||||
})
|
||||
|
||||
const [createResetPasswordToken] = useMutation(CREATE_RESET_PASSWORD_TOKEN, {
|
||||
onCompleted: ({ createResetPasswordToken: token }) => {
|
||||
setResetPasswordUrl(
|
||||
`https://localhost:3001/resetpassword?t=${token.token}`
|
||||
)
|
||||
toggleResetPasswordModal()
|
||||
}
|
||||
})
|
||||
|
||||
const [createReset2FAToken] = useMutation(CREATE_RESET_2FA_TOKEN, {
|
||||
onCompleted: ({ createReset2FAToken: token }) => {
|
||||
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
|
||||
toggleReset2FAModal()
|
||||
}
|
||||
})
|
||||
|
||||
const [userInfo, setUserInfo] = useState(null)
|
||||
|
||||
const [showCreateUserModal, setShowCreateUserModal] = useState(false)
|
||||
|
|
@ -102,81 +117,18 @@ const Users = () => {
|
|||
const toggleReset2FAModal = () => setShowReset2FAModal(!showReset2FAModal)
|
||||
|
||||
const [showRoleModal, setShowRoleModal] = useState(false)
|
||||
const toggleRoleModal = () =>
|
||||
setShowRoleModal(!showRoleModal)
|
||||
const toggleRoleModal = () => setShowRoleModal(!showRoleModal)
|
||||
|
||||
const [showEnableUserModal, setShowEnableUserModal] = useState(false)
|
||||
const toggleEnableUserModal = () =>
|
||||
setShowEnableUserModal(!showEnableUserModal)
|
||||
|
||||
/* const [showDeleteUserModal, setShowDeleteUserModal] = useState(false)
|
||||
const toggleDeleteUserModal = () =>
|
||||
setShowDeleteUserModal(!showDeleteUserModal) */
|
||||
|
||||
const [showInputConfirmModal, setShowInputConfirmModal] = useState(false)
|
||||
const toggleInputConfirmModal = () =>
|
||||
setShowInputConfirmModal(!showInputConfirmModal)
|
||||
|
||||
const [action, setAction] = useState(null)
|
||||
|
||||
const requestNewPassword = userID => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/resetpassword`,
|
||||
data: {
|
||||
userID: userID
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
const token = res.data.token
|
||||
setResetPasswordUrl(
|
||||
`https://localhost:3001/resetpassword?t=${token.token}`
|
||||
)
|
||||
toggleResetPasswordModal()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) console.log('error')
|
||||
})
|
||||
}
|
||||
|
||||
const requestNew2FA = userID => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/reset2fa`,
|
||||
data: {
|
||||
userID: userID
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
const token = res.data.token
|
||||
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
|
||||
toggleReset2FAModal()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) console.log('error')
|
||||
})
|
||||
}
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Login',
|
||||
|
|
@ -249,10 +201,20 @@ const Users = () => {
|
|||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
if (u.role === 'superuser') {
|
||||
setAction(() => requestNewPassword.bind(null, u.id))
|
||||
setAction(() =>
|
||||
createResetPasswordToken.bind(null, {
|
||||
variables: {
|
||||
userID: u.id
|
||||
}
|
||||
})
|
||||
)
|
||||
toggleInputConfirmModal()
|
||||
} else {
|
||||
requestNewPassword(u.id)
|
||||
createResetPasswordToken({
|
||||
variables: {
|
||||
userID: u.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -263,10 +225,20 @@ const Users = () => {
|
|||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
if (u.role === 'superuser') {
|
||||
setAction(() => requestNew2FA.bind(null, u.id))
|
||||
setAction(() => () =>
|
||||
createReset2FAToken({
|
||||
variables: {
|
||||
userID: u.id
|
||||
}
|
||||
})
|
||||
)
|
||||
toggleInputConfirmModal()
|
||||
} else {
|
||||
requestNew2FA(u.id)
|
||||
createReset2FAToken({
|
||||
variables: {
|
||||
userID: u.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -274,18 +246,6 @@ const Users = () => {
|
|||
)
|
||||
}
|
||||
},
|
||||
/* {
|
||||
header: 'Actions',
|
||||
width: 535,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: u => {
|
||||
const ua = parser(u.last_accessed_from)
|
||||
return u.last_accessed_from
|
||||
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
|
||||
: `No Record`
|
||||
}
|
||||
}, */
|
||||
{
|
||||
header: 'Enabled',
|
||||
width: 100,
|
||||
|
|
@ -302,22 +262,7 @@ const Users = () => {
|
|||
value={u.enabled}
|
||||
/>
|
||||
)
|
||||
}/* ,
|
||||
{
|
||||
header: 'Delete',
|
||||
width: 100,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: u => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
toggleDeleteUserModal()
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)
|
||||
} */
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
|
|
@ -366,14 +311,6 @@ const Users = () => {
|
|||
inputConfirmToggle={toggleInputConfirmModal}
|
||||
setAction={setAction}
|
||||
/>
|
||||
{/* <DeleteUserModal
|
||||
showModal={showDeleteUserModal}
|
||||
toggleModal={toggleDeleteUserModal}
|
||||
user={userInfo}
|
||||
confirm={deleteUser}
|
||||
inputConfirmToggle={toggleInputConfirmModal}
|
||||
setAction={setAction}
|
||||
/> */}
|
||||
<Input2FAModal
|
||||
showModal={showInputConfirmModal}
|
||||
toggleModal={toggleInputConfirmModal}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import {
|
|||
|
||||
const styles = {
|
||||
footer: {
|
||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [['auto', 0, spacer * 3, 0]]
|
||||
},
|
||||
modalTitle: {
|
||||
marginTop: -5,
|
||||
|
|
@ -42,9 +44,8 @@ const styles = {
|
|||
},
|
||||
copyToClipboard: {
|
||||
marginLeft: 'auto',
|
||||
paddingTop: 6,
|
||||
paddingLeft: 15,
|
||||
marginRight: -11
|
||||
paddingTop: 7,
|
||||
marginRight: -5
|
||||
},
|
||||
chip: {
|
||||
backgroundColor: subheaderColor,
|
||||
|
|
@ -63,10 +64,12 @@ const styles = {
|
|||
},
|
||||
addressWrapper: {
|
||||
backgroundColor: subheaderColor,
|
||||
marginTop: 8
|
||||
marginTop: 8,
|
||||
height: 35
|
||||
},
|
||||
address: {
|
||||
margin: `${spacer * 1.5}px ${spacer * 3}px`
|
||||
margin: `0px ${spacer * 2}px 0px ${spacer * 2}px`,
|
||||
paddingRight: -15
|
||||
},
|
||||
errorMessage: {
|
||||
fontFamily: fontSecondary,
|
||||
|
|
@ -75,6 +78,33 @@ const styles = {
|
|||
codeContainer: {
|
||||
marginTop: 15,
|
||||
marginBottom: 15
|
||||
},
|
||||
form: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
},
|
||||
submit: {
|
||||
margin: [['auto', 0, 0, 'auto']]
|
||||
},
|
||||
error: {
|
||||
color: errorColor
|
||||
},
|
||||
link: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 0,
|
||||
bottom: '-20px',
|
||||
right: '-20px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
width: '92.5%'
|
||||
},
|
||||
test1: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from 'react'
|
|||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { H2, Info3 } from 'src/components/typography'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
|
|
@ -28,18 +28,21 @@ const ChangeRoleModal = ({
|
|||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={275}
|
||||
width={450}
|
||||
height={250}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Change {user.username}'s role?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Change {user.username}'s role?
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
You are about to alter {user.username}'s role. This will change this
|
||||
user's permission to access certain resources.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||
</P>
|
||||
<P className={classes.info}>Do you wish to proceed?</P>
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
className={classes.submit}
|
||||
onClick={() => {
|
||||
setAction(() =>
|
||||
confirm.bind(null, {
|
||||
|
|
@ -52,7 +55,7 @@ const ChangeRoleModal = ({
|
|||
inputConfirmToggle()
|
||||
handleClose()
|
||||
}}>
|
||||
Finish
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,48 @@
|
|||
import { useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import classnames from 'classnames'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { RadioGroup } from 'src/components/inputs'
|
||||
import { TextInput } from 'src/components/inputs/base'
|
||||
import { TextInput, RadioGroup } from 'src/components/inputs/formik'
|
||||
import { H1, H2, H3, Info3, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CREATE_USER = gql`
|
||||
mutation createRegisterToken($username: String!, $role: String!) {
|
||||
createRegisterToken(username: $username, role: $role) {
|
||||
token
|
||||
expire
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
username: Yup.string()
|
||||
.email('Username field should be in an email format!')
|
||||
.required('Username field is required!'),
|
||||
role: Yup.string().required('Role field is required!')
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
username: '',
|
||||
role: ''
|
||||
}
|
||||
|
||||
const CreateUserModal = ({ showModal, toggleModal }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [usernameField, setUsernameField] = useState('')
|
||||
const [roleField, setRoleField] = useState('')
|
||||
const [createUserURL, setCreateUserURL] = useState(null)
|
||||
const [invalidUser, setInvalidUser] = useState(false)
|
||||
|
||||
const radioOptions = [
|
||||
{
|
||||
|
|
@ -35,59 +55,27 @@ const CreateUserModal = ({ showModal, toggleModal }) => {
|
|||
}
|
||||
]
|
||||
|
||||
const handleUsernameChange = event => {
|
||||
if (event.target.value === '') {
|
||||
setInvalidUser(false)
|
||||
}
|
||||
setUsernameField(event.target.value)
|
||||
}
|
||||
|
||||
const handleRoleChange = event => {
|
||||
setRoleField(event.target.value)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setUsernameField('')
|
||||
setRoleField('')
|
||||
setInvalidUser(false)
|
||||
setCreateUserURL(null)
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
const handleCreateUser = () => {
|
||||
const username = usernameField.trim()
|
||||
|
||||
if (username === '') {
|
||||
setInvalidUser(true)
|
||||
return
|
||||
}
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/createuser`,
|
||||
data: {
|
||||
username: username,
|
||||
role: roleField
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
const message = res.data.message
|
||||
if (status === 200 && message) setInvalidUser(true)
|
||||
if (status === 200 && !message) {
|
||||
const token = res.data.token
|
||||
const [createUser, { error }] = useMutation(CREATE_USER, {
|
||||
onCompleted: ({ createRegisterToken: token }) => {
|
||||
setCreateUserURL(`https://localhost:3001/register?t=${token.token}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) console.log('error')
|
||||
|
||||
const roleClass = (formikErrors, formikTouched) => ({
|
||||
[classes.error]: formikErrors.role && formikTouched.role
|
||||
})
|
||||
|
||||
const getErrorMsg = (formikErrors, formikTouched) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (error) return 'Internal server error'
|
||||
if (formikErrors.username && formikTouched.username)
|
||||
return formikErrors.username
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -99,38 +87,60 @@ const CreateUserModal = ({ showModal, toggleModal }) => {
|
|||
height={400}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<Formik
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={values => {
|
||||
setUsernameField(values.username)
|
||||
createUser({
|
||||
variables: { username: values.username, role: values.role }
|
||||
})
|
||||
}}>
|
||||
{({ errors, touched }) => (
|
||||
<Form id="register-user-form" className={classes.form}>
|
||||
<H1 className={classes.modalTitle}>Create new user</H1>
|
||||
<H3 className={classes.modalLabel1}>User login</H3>
|
||||
<TextInput
|
||||
error={invalidUser}
|
||||
<Field
|
||||
component={TextInput}
|
||||
name="username"
|
||||
autoFocus
|
||||
id="username"
|
||||
type="text"
|
||||
size="lg"
|
||||
width={338}
|
||||
onChange={handleUsernameChange}
|
||||
value={usernameField}
|
||||
autoFocus
|
||||
label="User login"
|
||||
/>
|
||||
<H3 className={classes.modalLabel2}>Role</H3>
|
||||
<RadioGroup
|
||||
name="userrole"
|
||||
value={roleField}
|
||||
options={radioOptions}
|
||||
onChange={handleRoleChange}
|
||||
className={classes.radioGroup}
|
||||
<H3
|
||||
className={classnames(
|
||||
roleClass(errors, touched),
|
||||
classes.modalLabel2
|
||||
)}>
|
||||
Role
|
||||
</H3>
|
||||
<Field
|
||||
component={RadioGroup}
|
||||
name="role"
|
||||
labelClassName={classes.radioLabel}
|
||||
className={classes.radioGroup}
|
||||
options={radioOptions}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
<Button onClick={handleCreateUser}>Finish</Button>
|
||||
{getErrorMsg(errors, touched) && (
|
||||
<ErrorMessage>{getErrorMsg(errors, touched)}</ErrorMessage>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="register-user-form"
|
||||
className={classes.submit}>
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)}
|
||||
{showModal && createUserURL && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={215}
|
||||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Creating {usernameField}...</H2>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from 'react'
|
|||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { H2, Info3 } from 'src/components/typography'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
|
|
@ -32,16 +32,17 @@ const DeleteUserModal = ({
|
|||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Delete {user.username}?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
<Info2 className={classes.modalTitle}>Delete {user.username}?</Info2>
|
||||
<P className={classes.info}>
|
||||
You are about to delete {user.username}. This will remove existent
|
||||
sessions and revoke this user's permissions to access the system.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>
|
||||
</P>
|
||||
<P className={classes.info}>
|
||||
This is a <b>PERMANENT</b> operation. Do you wish to proceed?
|
||||
</Info3>
|
||||
</P>
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
className={classes.submit}
|
||||
onClick={() => {
|
||||
if (user.role === 'superuser') {
|
||||
setAction(() =>
|
||||
|
|
@ -61,7 +62,7 @@ const DeleteUserModal = ({
|
|||
}
|
||||
handleClose()
|
||||
}}>
|
||||
Finish
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from 'react'
|
|||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { H2, Info3 } from 'src/components/typography'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
|
|
@ -28,34 +28,39 @@ const EnableUserModal = ({
|
|||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
width={450}
|
||||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
{!user.enabled && (
|
||||
<>
|
||||
<H2 className={classes.modalTitle}>Enable {user.username}?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Enable {user.username}?
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
You are about to enable {user.username} into the system,
|
||||
activating previous eligible sessions and grant permissions to
|
||||
access the system.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||
</P>
|
||||
<P className={classes.info}>Do you wish to proceed?</P>
|
||||
</>
|
||||
)}
|
||||
{user.enabled && (
|
||||
<>
|
||||
<H2 className={classes.modalTitle}>Disable {user.username}?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Disable {user.username}?
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
You are about to disable {user.username} from the system,
|
||||
deactivating previous eligible sessions and removing permissions
|
||||
to access the system.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||
</P>
|
||||
<P className={classes.info}>Do you wish to proceed?</P>
|
||||
</>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
className={classes.submit}
|
||||
onClick={() => {
|
||||
if (user.role === 'superuser') {
|
||||
setAction(() =>
|
||||
|
|
@ -75,7 +80,7 @@ const EnableUserModal = ({
|
|||
}
|
||||
handleClose()
|
||||
}}>
|
||||
Finish
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import { useLazyQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { CodeInput } from 'src/components/inputs/base'
|
||||
import { H2, Info3, P } from 'src/components/typography'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CONFIRM_2FA = gql`
|
||||
query confirm2FA($code: String!) {
|
||||
confirm2FA(code: $code)
|
||||
}
|
||||
`
|
||||
|
||||
const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -31,32 +35,23 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
|
|||
toggleModal()
|
||||
}
|
||||
|
||||
const handleActionConfirm = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/confirm2fa`,
|
||||
data: {
|
||||
code: twoFACode
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
const [confirm2FA, { error: queryError }] = useLazyQuery(CONFIRM_2FA, {
|
||||
onCompleted: ({ confirm2FA: success }) => {
|
||||
if (!success) {
|
||||
setInvalidCode(true)
|
||||
} else {
|
||||
action()
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
const errStatus = err.response.status
|
||||
if (errStatus === 401) setInvalidCode(true)
|
||||
})
|
||||
|
||||
const getErrorMsg = () => {
|
||||
if (queryError) return 'Internal server error'
|
||||
if (twoFACode.length !== 6 && invalidCode)
|
||||
return 'The code should have 6 characters!'
|
||||
if (invalidCode) return 'Code is invalid. Please try again.'
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -64,15 +59,15 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
|
|||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={400}
|
||||
width={500}
|
||||
height={350}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Confirm action</H2>
|
||||
<Info3 className={classes.info}>
|
||||
Please confirm this action by placing your two-factor authentication
|
||||
code below.
|
||||
</Info3>
|
||||
<Info2 className={classes.modalTitle}>Confirm action</Info2>
|
||||
<P className={classes.info}>
|
||||
To make changes on this user, please confirm this action by entering
|
||||
your two-factor authentication code below.
|
||||
</P>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFACode}
|
||||
|
|
@ -82,13 +77,21 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
|
|||
containerStyle={classes.codeContainer}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
{invalidCode && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
<Button onClick={handleActionConfirm}>Finish</Button>
|
||||
<Button
|
||||
className={classes.submit}
|
||||
onClick={() => {
|
||||
if (twoFACode.length !== 6) {
|
||||
setInvalidCode(true)
|
||||
return
|
||||
}
|
||||
confirm2FA({ variables: { code: twoFACode } })
|
||||
}}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { H2, Info3, Mono } from 'src/components/typography'
|
||||
import { Info2, P, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
|
@ -21,19 +21,24 @@ const Reset2FAModal = ({ showModal, toggleModal, reset2FAURL, user }) => {
|
|||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={215}
|
||||
width={500}
|
||||
height={200}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Reset 2FA for {user.username}</H2>
|
||||
<Info3 className={classes.info}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Reset 2FA for {user.username}
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
Safely share this link with {user.username} for a two-factor
|
||||
authentication reset.
|
||||
</Info3>
|
||||
</P>
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||
<CopyToClipboard
|
||||
className={classes.link}
|
||||
buttonClassname={classes.copyToClipboard}
|
||||
wrapperClassname={classes.test1}>
|
||||
{reset2FAURL}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { H2, Info3, Mono } from 'src/components/typography'
|
||||
import { Info2, P, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
|
@ -26,20 +26,23 @@ const ResetPasswordModal = ({
|
|||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={215}
|
||||
width={500}
|
||||
height={180}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Reset password for {user.username}
|
||||
</H2>
|
||||
<Info3 className={classes.info}>
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
Safely share this link with {user.username} for a password reset.
|
||||
</Info3>
|
||||
</P>
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||
<CopyToClipboard
|
||||
className={classes.link}
|
||||
buttonClassname={classes.copyToClipboard}
|
||||
wrapperClassname={classes.test1}>
|
||||
{resetPasswordURL}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import {
|
|||
MuiThemeProvider,
|
||||
makeStyles
|
||||
} from '@material-ui/core/styles'
|
||||
import { axios } from '@use-hooks/axios'
|
||||
import { create } from 'jss'
|
||||
import extendJss from 'jss-plugin-extend'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
useLocation,
|
||||
useHistory,
|
||||
|
|
@ -72,7 +73,7 @@ const Main = () => {
|
|||
const classes = useStyles()
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
const { wizardTested } = useContext(AppContext)
|
||||
const { wizardTested, userData } = useContext(AppContext)
|
||||
|
||||
const route = location.pathname
|
||||
|
||||
|
|
@ -91,7 +92,9 @@ const Main = () => {
|
|||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{!is404 && wizardTested && <Header tree={tree} />}
|
||||
{!is404 && wizardTested && userData && (
|
||||
<Header tree={tree} user={userData} />
|
||||
)}
|
||||
<main className={classes.wrapper}>
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
|
|
@ -117,9 +120,36 @@ const Main = () => {
|
|||
|
||||
const App = () => {
|
||||
const [wizardTested, setWizardTested] = useState(false)
|
||||
const [userData, setUserData] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
useEffect(() => {
|
||||
getUserData()
|
||||
}, [])
|
||||
|
||||
const getUserData = () => {
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: `${url}/user-data`,
|
||||
withCredentials: true
|
||||
})
|
||||
.then(res => {
|
||||
setLoading(false)
|
||||
if (res.status === 200) setUserData(res.data.user)
|
||||
})
|
||||
.catch(err => {
|
||||
setLoading(false)
|
||||
if (err.status === 403) setUserData(null)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ wizardTested, setWizardTested }}>
|
||||
<AppContext.Provider
|
||||
value={{ wizardTested, setWizardTested, userData, setUserData }}>
|
||||
{!loading && (
|
||||
<Router>
|
||||
<ApolloProvider>
|
||||
<StylesProvider jss={jss}>
|
||||
|
|
@ -130,6 +160,7 @@ const App = () => {
|
|||
</StylesProvider>
|
||||
</ApolloProvider>
|
||||
</Router>
|
||||
)}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
|||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||
import ServerLogs from 'src/pages/ServerLogs'
|
||||
import Services from 'src/pages/Services/Services'
|
||||
// import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
|
||||
import SessionManagement from 'src/pages/SessionManagement/SessionManagement'
|
||||
import Transactions from 'src/pages/Transactions/Transactions'
|
||||
import Triggers from 'src/pages/Triggers'
|
||||
|
|
@ -247,6 +248,7 @@ const tree = [
|
|||
key: 'promo-codes',
|
||||
label: 'Promo Codes',
|
||||
route: '/compliance/loyalty/codes',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: PromoCodes
|
||||
},
|
||||
{
|
||||
|
|
@ -397,12 +399,12 @@ const Routes = () => {
|
|||
<PrivateRoute path="/machines" component={Machines} />
|
||||
<PrivateRoute path="/wizard" component={Wizard} />
|
||||
<Route path="/register" component={Register} />
|
||||
{/* <Route path="/configmigration" component={ConfigMigration} /> */}
|
||||
<PublicRoute path="/login" restricted component={Login} />
|
||||
<Route path="/resetpassword" component={ResetPassword} />
|
||||
<Route path="/reset2fa" component={Reset2FA} />
|
||||
{/* <Route path="/configmigration" component={ConfigMigration} /> */}
|
||||
{getFilteredRoutes().map(({ route, component: Page, key }) => (
|
||||
<Route path={route} key={key}>
|
||||
<PrivateRoute path={route} key={key}>
|
||||
<Transition
|
||||
className={classes.wrapper}
|
||||
{...transitionProps}
|
||||
|
|
@ -417,7 +419,7 @@ const Routes = () => {
|
|||
</div>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</PrivateRoute>
|
||||
))}
|
||||
<Route path="/404" />
|
||||
<Route path="*">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { HttpLink } from 'apollo-link-http'
|
|||
import React, { useContext } from 'react'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
|
||||
import { AppContext } from 'src/App'
|
||||
import AppContext from 'src/AppContext'
|
||||
|
||||
const URI =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
|
|
|||
47
package-lock.json
generated
47
package-lock.json
generated
|
|
@ -24,6 +24,14 @@
|
|||
"zen-observable": "^0.8.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wry/equality": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.1.tgz",
|
||||
"integrity": "sha512-8/Ftr3jUZ4EXhACfSwPIfNsE8V6WKesdjp+Dxi78Bej6qlasAxiz0/F8j0miACRj9CL4vC5Y5FsfwwEYAuhWbg==",
|
||||
"requires": {
|
||||
"tslib": "^1.14.1"
|
||||
}
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz",
|
||||
|
|
@ -512,6 +520,13 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-syntax-nullish-coalescing-operator": {
|
||||
|
|
@ -538,6 +553,13 @@
|
|||
"integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-syntax-optional-catch-binding": {
|
||||
|
|
@ -757,6 +779,11 @@
|
|||
"version": "0.13.7",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -768,6 +795,18 @@
|
|||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
|
||||
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
|
|
@ -2531,14 +2570,6 @@
|
|||
"tslib": "^1.14.1"
|
||||
}
|
||||
},
|
||||
"@wry/equality": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.1.tgz",
|
||||
"integrity": "sha512-8/Ftr3jUZ4EXhACfSwPIfNsE8V6WKesdjp+Dxi78Bej6qlasAxiz0/F8j0miACRj9CL4vC5Y5FsfwwEYAuhWbg==",
|
||||
"requires": {
|
||||
"tslib": "^1.14.1"
|
||||
}
|
||||
},
|
||||
"@wry/trie": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.2.1.tgz",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue