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