feat: add user management screen
feat: login screen fix: login routing and layout feat: add users migration feat: passport login strategy fix: users migration feat: simple authentication fix: request body feat: JWT authorization feat: 2fa step on login feat: 2fa flow feat: add rememberme to req body fix: hide 2fa secret from jwt fix: block login access to logged in user fix: rerouting to wizard refactor: login screen feat: setup 2fa state on login feat: 2fa secret qr code fix: remove jwt from 2fa secret fix: wizard redirect after login fix: 2fa setup flow fix: user id to uuid feat: user roles feat: user sessions and db persistence feat: session saving on DB and cookie refactor: unused code feat: cookie auto renew on request feat: get user data endpoint fix: repeated requests feat: react routing fix: private routes refactor: auth feat: sessions aware of ua and ip feat: sessions on gql feat: session management screen feat: replace user_tokens usage for users feat: user deletion also deletes active sessions feat: remember me alters session cookie accordingly feat: last session by all users fix: login feedback fix: page loading UX feat: routes based on user role feat: header aware of roles feat: reset password fix: reset password endpoint feat: handle password change feat: reset 2FA feat: user role on management screen feat: change user role fix: user last session query fix: context fix: destroy own session feat: reset password now resets sessions feat: reset 2fa now resets sessions refactor: user data refactor: user management screen feat: user enable feat: schema directives fix: remove schema directive temp feat: create new users feat: register endpoint feat: modals for reset links fix: directive Date errors feat: superuser directive feat: create user url modal fix: user management layout feat: confirmation modals fix: info text feat: 2fa input component feat: code input on 2fa state feat: add button styling feat: confirmation modal on superuser action feat: rework 2fa setup screen feat: rework reset 2fa screen fix: session management screen fix: user management screen fix: blacklist roles chore: migrate old customer values to new columns fix: value migration fix: value migration refactor: remove old code
This commit is contained in:
parent
368781864e
commit
fded22f39a
50 changed files with 9839 additions and 4501 deletions
|
|
@ -8,14 +8,20 @@ const cors = require('cors')
|
||||||
const helmet = require('helmet')
|
const helmet = require('helmet')
|
||||||
const nocache = require('nocache')
|
const nocache = require('nocache')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
const { ApolloServer, AuthenticationError } = require('apollo-server-express')
|
const { ApolloServer, AuthenticationError } = require('apollo-server-express')
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
const session = require('express-session')
|
||||||
|
const pgSession = require('connect-pg-simple')(session)
|
||||||
|
|
||||||
const { typeDefs, resolvers } = require('./graphql/schema')
|
|
||||||
const login = require('./services/login')
|
const login = require('./services/login')
|
||||||
const register = require('./routes/authentication')
|
const register = require('./routes/authentication')
|
||||||
|
|
||||||
const options = require('../options')
|
const options = require('../options')
|
||||||
|
const db = require('../db')
|
||||||
|
const users = require('../users')
|
||||||
|
|
||||||
|
const { typeDefs, resolvers, AuthDirective, SuperuserDirective } = 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)
|
||||||
|
|
@ -32,11 +38,35 @@ app.use(helmet())
|
||||||
app.use(compression())
|
app.use(compression())
|
||||||
app.use(nocache())
|
app.use(nocache())
|
||||||
app.use(cookieParser())
|
app.use(cookieParser())
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true })) // support encoded bodies
|
||||||
app.use(express.static(path.resolve(__dirname, '..', '..', 'public')))
|
app.use(express.static(path.resolve(__dirname, '..', '..', 'public')))
|
||||||
|
|
||||||
|
app.use(['*'], session({
|
||||||
|
store: new pgSession({
|
||||||
|
pgPromise: db,
|
||||||
|
tableName: 'user_sessions'
|
||||||
|
}),
|
||||||
|
name: 'lid',
|
||||||
|
secret: 'MY_SECRET',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
domain: hostname,
|
||||||
|
sameSite: true,
|
||||||
|
maxAge: 60 * 10 * 1000 // 10 minutes
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
const apolloServer = new ApolloServer({
|
const apolloServer = new ApolloServer({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
|
schemaDirectives: {
|
||||||
|
auth: AuthDirective,
|
||||||
|
superuser: SuperuserDirective
|
||||||
|
},
|
||||||
playground: false,
|
playground: false,
|
||||||
introspection: false,
|
introspection: false,
|
||||||
formatError: error => {
|
formatError: error => {
|
||||||
|
|
@ -44,10 +74,19 @@ const apolloServer = new ApolloServer({
|
||||||
return error
|
return error
|
||||||
},
|
},
|
||||||
context: async ({ req }) => {
|
context: async ({ req }) => {
|
||||||
const token = req.cookies && req.cookies.token
|
if (!req.session.user) throw new AuthenticationError('Authentication failed')
|
||||||
|
const user = await users.verifyAndUpdateUser(
|
||||||
|
req.session.user.id,
|
||||||
|
req.headers['user-agent'] || 'Unknown',
|
||||||
|
req.ip
|
||||||
|
)
|
||||||
|
if (!user || !user.enabled) throw new AuthenticationError('Authentication failed')
|
||||||
|
|
||||||
const success = await login.authenticate(token)
|
req.session.ua = req.headers['user-agent'] || 'Unknown'
|
||||||
if (!success) throw new AuthenticationError('Authentication failed')
|
req.session.ipAddress = req.ip
|
||||||
|
req.session.lastUsed = new Date(Date.now()).toISOString()
|
||||||
|
req.session.user.id = user.id
|
||||||
|
req.session.user.role = user.role
|
||||||
return { req: { ...req } }
|
return { req: { ...req } }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -67,6 +106,8 @@ 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('/api', register)
|
||||||
|
|
||||||
|
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')))
|
||||||
|
|
||||||
|
|
|
||||||
259
lib/new-admin/routes/auth.js
Normal file
259
lib/new-admin/routes/auth.js
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
const otplib = require('otplib')
|
||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
|
||||||
|
const users = require('../../users')
|
||||||
|
const login = require('../login')
|
||||||
|
|
||||||
|
async function isValidUser (username, password) {
|
||||||
|
const hashedPassword = await login.checkUser(username)
|
||||||
|
if (!hashedPassword) return false
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(password, hashedPassword)
|
||||||
|
if (!isMatch) return false
|
||||||
|
|
||||||
|
const user = await login.validateUser(username, hashedPassword)
|
||||||
|
if (!user) return false
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function (app) {
|
||||||
|
app.post('/api/login', function (req, res, next) {
|
||||||
|
const usernameInput = req.body.username
|
||||||
|
const passwordInput = req.body.password
|
||||||
|
|
||||||
|
isValidUser(usernameInput, passwordInput).then(user => {
|
||||||
|
if (!user) return res.sendStatus(403)
|
||||||
|
users.get2FASecret(user.id).then(user => {
|
||||||
|
const twoFASecret = user.twofa_code
|
||||||
|
if (twoFASecret) return res.status(200).json({ message: 'INPUT2FA' })
|
||||||
|
if (!twoFASecret) return res.status(200).json({ message: 'SETUP2FA' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/login/2fa', function (req, res, next) {
|
||||||
|
const code = req.body.twoFACode
|
||||||
|
const username = req.body.username
|
||||||
|
const password = req.body.password
|
||||||
|
const rememberMeInput = req.body.rememberMe
|
||||||
|
|
||||||
|
isValidUser(username, password).then(user => {
|
||||||
|
if (!user) return res.sendStatus(403)
|
||||||
|
|
||||||
|
users.get2FASecret(user.id).then(user => {
|
||||||
|
const secret = user.twofa_code
|
||||||
|
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||||
|
if (!isCodeValid) return res.sendStatus(403)
|
||||||
|
|
||||||
|
const finalUser = { id: user.id, username: user.username, role: user.role }
|
||||||
|
req.session.user = finalUser
|
||||||
|
if (rememberMeInput) req.session.cookie.maxAge = 90 * 24 * 60 * 60 * 1000 // 90 days
|
||||||
|
|
||||||
|
return res.sendStatus(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/login/2fa/setup', function (req, res, next) {
|
||||||
|
const username = req.body.username
|
||||||
|
const password = req.body.password
|
||||||
|
|
||||||
|
// TODO: maybe check if the user already has a 2fa secret
|
||||||
|
isValidUser(username, password).then(user => {
|
||||||
|
if (!user) return res.sendStatus(403)
|
||||||
|
|
||||||
|
const secret = otplib.authenticator.generateSecret()
|
||||||
|
const otpauth = otplib.authenticator.keyuri(username, 'Lamassu Industries', secret)
|
||||||
|
return res.status(200).json({ secret, otpauth })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/login/2fa/save', function (req, res, next) {
|
||||||
|
const username = req.body.username
|
||||||
|
const password = req.body.password
|
||||||
|
const secret = req.body.secret
|
||||||
|
const code = req.body.code
|
||||||
|
|
||||||
|
isValidUser(username, password).then(user => {
|
||||||
|
if (!user || !secret) return res.sendStatus(403)
|
||||||
|
|
||||||
|
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||||
|
if (!isCodeValid) return res.sendStatus(403)
|
||||||
|
|
||||||
|
users.save2FASecret(user.id, secret)
|
||||||
|
return res.sendStatus(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/user-data', function (req, res, next) {
|
||||||
|
const 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,48 +1,20 @@
|
||||||
const crypto = require('crypto')
|
|
||||||
|
|
||||||
const db = require('../../db')
|
const db = require('../../db')
|
||||||
|
|
||||||
function generateOTP (name) {
|
function checkUser (username) {
|
||||||
const otp = crypto.randomBytes(32).toString('hex')
|
const sql = 'select * from users where username=$1'
|
||||||
|
return db.oneOrNone(sql, [username]).then(value => { return value.password }).catch(() => false)
|
||||||
const sql = 'insert into one_time_passes (token, name) values ($1, $2)'
|
|
||||||
|
|
||||||
return db.none(sql, [otp, name])
|
|
||||||
.then(() => otp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOTP (otp) {
|
function validateUser (username, password) {
|
||||||
const sql = `delete from one_time_passes
|
const sql = 'select id, username from users where username=$1 and password=$2'
|
||||||
where token=$1
|
const sqlUpdateLastAccessed = 'update users set last_accessed = now() where username=$1'
|
||||||
returning name, created < now() - interval '1 hour' as expired`
|
|
||||||
|
|
||||||
return db.one(sql, [otp])
|
return db.oneOrNone(sql, [username, password])
|
||||||
.then(r => ({ success: !r.expired, expired: r.expired, name: r.name }))
|
.then(user => { db.none(sqlUpdateLastAccessed, [user.username]); return user })
|
||||||
.catch(() => ({ success: false, expired: false }))
|
.catch(() => false)
|
||||||
}
|
|
||||||
|
|
||||||
function register (otp, ua, ip) {
|
|
||||||
return validateOTP(otp)
|
|
||||||
.then(r => {
|
|
||||||
if (!r.success) return r
|
|
||||||
|
|
||||||
const token = crypto.randomBytes(32).toString('hex')
|
|
||||||
const sql = 'insert into user_tokens (token, name, user_agent, ip_address) values ($1, $2, $3, $4)'
|
|
||||||
|
|
||||||
return db.none(sql, [token, r.name, ua, ip])
|
|
||||||
.then(() => ({ success: true, token: token }))
|
|
||||||
})
|
|
||||||
.catch(() => ({ success: false, expired: false }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function authenticate (token) {
|
|
||||||
const sql = 'select token from user_tokens where token=$1'
|
|
||||||
|
|
||||||
return db.one(sql, [token]).then(() => true).catch(() => false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateOTP,
|
checkUser,
|
||||||
register,
|
validateUser
|
||||||
authenticate
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
lib/session-manager.js
Normal file
42
lib/session-manager.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
function getSessionList () {
|
||||||
|
const sql = `select * from user_sessions order by sess -> 'user' ->> 'username'`
|
||||||
|
return db.any(sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastSessionByUser () {
|
||||||
|
const sql = `select b.username, a.user_agent, a.ip_address, a.last_used, b.role from (
|
||||||
|
select sess -> 'user' ->> 'username' as username,
|
||||||
|
sess ->> 'ua' as user_agent,
|
||||||
|
sess ->> 'ipAddress' as ip_address,
|
||||||
|
sess ->> 'lastUsed' as last_used
|
||||||
|
from user_sessions
|
||||||
|
) a right join (
|
||||||
|
select distinct on (username)
|
||||||
|
username, role
|
||||||
|
from users) b on a.username = b.username`
|
||||||
|
return db.any(sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserSessions (username) {
|
||||||
|
const sql = `select * from user_sessions where sess -> 'user' ->> 'username'=$1`
|
||||||
|
return db.any(sql, [username])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSession (sessionID) {
|
||||||
|
const sql = `select * from user_sessions where sid=$1`
|
||||||
|
return db.any(sql, [sessionID])
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUserSessions (username) {
|
||||||
|
const sql = `delete from user_sessions where sess -> 'user' ->> 'username'=$1`
|
||||||
|
return db.none(sql, [username])
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSession (sessionID) {
|
||||||
|
const sql = `delete from user_sessions where sid=$1`
|
||||||
|
return db.none(sql, [sessionID])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getSessionList, getLastSessionByUser, getUserSessions, getSession, deleteUserSessions, deleteSession }
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
const db = require('./db')
|
|
||||||
|
|
||||||
function getTokenList () {
|
|
||||||
const sql = `select * from user_tokens`
|
|
||||||
return db.any(sql)
|
|
||||||
}
|
|
||||||
|
|
||||||
function revokeToken (token) {
|
|
||||||
const sql = `delete from user_tokens where token = $1`
|
|
||||||
return db.none(sql, [token])
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { getTokenList, revokeToken }
|
|
||||||
144
lib/users.js
144
lib/users.js
|
|
@ -1,5 +1,8 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const pgp = require('pg-promise')()
|
const pgp = require('pg-promise')()
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
const uuid = require('uuid')
|
||||||
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
|
||||||
|
|
@ -33,4 +36,143 @@ function getByIds (tokens) {
|
||||||
const tokensClause = _.map(pgp.as.text, tokens).join(',')
|
const tokensClause = _.map(pgp.as.text, tokens).join(',')
|
||||||
return db.any(sql, [tokensClause])
|
return db.any(sql, [tokensClause])
|
||||||
}
|
}
|
||||||
module.exports = { get, getByIds }
|
|
||||||
|
function getUsers () {
|
||||||
|
const sql = `select id, username, role, enabled, last_accessed, last_accessed_from, last_accessed_address from users order by username`
|
||||||
|
return db.any(sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByName (username) {
|
||||||
|
const sql = `select id, username, role, last_accessed from users where username=$1 limit 1`
|
||||||
|
return db.oneOrNone(sql, [username])
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyAndUpdateUser (id, ua, ip) {
|
||||||
|
const sql = `select id, username, role, enabled from users where id=$1 limit 1`
|
||||||
|
return db.oneOrNone(sql, [id]).then(user => {
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
const sql2 = `update users set last_accessed=now(), last_accessed_from=$1, last_accessed_address=$2 where id=$3 returning id, role, enabled`
|
||||||
|
return db.one(sql2, [ua, ip, id]).then(user => {
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUser (username, password, role) {
|
||||||
|
const sql = `insert into users (id, username, password, role) values ($1, $2, $3, $4)`
|
||||||
|
bcrypt.hash(password, 12).then(function (hash) {
|
||||||
|
return db.none(sql, [uuid.v4(), username, hash, role])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUser (id) {
|
||||||
|
const sql = `delete from users where id=$1`
|
||||||
|
const sql2 = `delete from user_sessions where sess -> 'user' ->> 'id'=$1`
|
||||||
|
|
||||||
|
return db.none(sql, [id]).then(() => db.none(sql2, [id]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function findById (id) {
|
||||||
|
const sql = 'select id, username from users where id=$1'
|
||||||
|
return db.oneOrNone(sql, [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
function get2FASecret (id) {
|
||||||
|
const sql = 'select id, username, twofa_code, role from users where id=$1'
|
||||||
|
return db.oneOrNone(sql, [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
function save2FASecret (id, secret) {
|
||||||
|
const sql = 'update users set twofa_code=$1 where id=$2'
|
||||||
|
const sql2 = `delete from user_sessions where sess -> 'user' ->> 'id'=$1`
|
||||||
|
return db.none(sql, [secret, id]).then(() => db.none(sql2, [id]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate2FAResetToken (token) {
|
||||||
|
const sql = `delete from reset_twofa
|
||||||
|
where token=$1
|
||||||
|
returning user_id, now() < expire as success`
|
||||||
|
|
||||||
|
return db.one(sql, [token])
|
||||||
|
.then(res => ({ userID: res.user_id, success: res.success }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReset2FAToken (userID) {
|
||||||
|
const token = crypto.randomBytes(32).toString('hex')
|
||||||
|
const sql = `insert into reset_twofa (token, user_id) values ($1, $2) on conflict (user_id) do update set token=$1, expire=now() + interval '30 minutes' returning *`
|
||||||
|
|
||||||
|
return db.one(sql, [token, userID])
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePassword (id, password) {
|
||||||
|
bcrypt.hash(password, 12).then(function (hash) {
|
||||||
|
const sql = `update users set password=$1 where id=$2`
|
||||||
|
const sql2 = `delete from user_sessions where sess -> 'user' ->> 'id'=$1`
|
||||||
|
return db.none(sql, [hash, id]).then(() => db.none(sql2, [id]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePasswordResetToken (token) {
|
||||||
|
const sql = `delete from reset_password
|
||||||
|
where token=$1
|
||||||
|
returning user_id, now() < expire as success`
|
||||||
|
|
||||||
|
return db.one(sql, [token])
|
||||||
|
.then(res => ({ userID: res.user_id, success: res.success }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResetPasswordToken (userID) {
|
||||||
|
const token = crypto.randomBytes(32).toString('hex')
|
||||||
|
const sql = `insert into reset_password (token, user_id) values ($1, $2) on conflict (user_id) do update set token=$1, expire=now() + interval '30 minutes' returning *`
|
||||||
|
|
||||||
|
return db.one(sql, [token, userID])
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUserRegistrationToken (username, role) {
|
||||||
|
const token = crypto.randomBytes(32).toString('hex')
|
||||||
|
const sql = `insert into user_register_tokens (token, username, role) values ($1, $2, $3) on conflict (username)
|
||||||
|
do update set token=$1, expire=now() + interval '30 minutes' returning *`
|
||||||
|
|
||||||
|
return db.one(sql, [token, username, role])
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUserRegistrationToken (token) {
|
||||||
|
const sql = `delete from user_register_tokens where token=$1
|
||||||
|
returning username, role, now() < expire as success`
|
||||||
|
|
||||||
|
return db.one(sql, [token])
|
||||||
|
.then(res => ({ username: res.username, role: res.role, success: res.success }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeUserRole (id, newRole) {
|
||||||
|
const sql = `update users set role=$1 where id=$2`
|
||||||
|
return db.none(sql, [newRole, id])
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserEnable (id) {
|
||||||
|
const sql = `update users set enabled=not enabled where id=$1`
|
||||||
|
return db.none(sql, [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get,
|
||||||
|
getByIds,
|
||||||
|
getUsers,
|
||||||
|
getByName,
|
||||||
|
verifyAndUpdateUser,
|
||||||
|
createUser,
|
||||||
|
deleteUser,
|
||||||
|
findById,
|
||||||
|
updatePassword,
|
||||||
|
get2FASecret,
|
||||||
|
save2FASecret,
|
||||||
|
validate2FAResetToken,
|
||||||
|
createReset2FAToken,
|
||||||
|
validatePasswordResetToken,
|
||||||
|
createResetPasswordToken,
|
||||||
|
createUserRegistrationToken,
|
||||||
|
validateUserRegistrationToken,
|
||||||
|
changeUserRole,
|
||||||
|
toggleUserEnable
|
||||||
|
}
|
||||||
|
|
|
||||||
13
migrations/1603804834628-add-last-accessed-tokens.js
Normal file
13
migrations/1603804834628-add-last-accessed-tokens.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
'ALTER TABLE user_tokens ADD COLUMN last_accessed timestamptz',
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
93
migrations/1605181184453-users.js
Normal file
93
migrations/1605181184453-users.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
var db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
`create type role as ENUM('user', 'superuser')`,
|
||||||
|
`create table users (
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
username varchar(50) UNIQUE,
|
||||||
|
password varchar(100),
|
||||||
|
role role default 'user',
|
||||||
|
enabled boolean default true,
|
||||||
|
twofa_code varchar(100),
|
||||||
|
created timestamptz not null default now(),
|
||||||
|
last_accessed timestamptz not null default now(),
|
||||||
|
last_accessed_from text,
|
||||||
|
last_accessed_address inet )`,
|
||||||
|
`CREATE TABLE "user_sessions" (
|
||||||
|
"sid" varchar NOT NULL COLLATE "default",
|
||||||
|
"sess" json NOT NULL,
|
||||||
|
"expire" timestamp(6) NOT NULL )
|
||||||
|
WITH (OIDS=FALSE)`,
|
||||||
|
`ALTER TABLE "user_sessions" ADD CONSTRAINT "session_pkey" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE`,
|
||||||
|
`CREATE INDEX "IDX_session_expire" ON "user_sessions" ("expire")`,
|
||||||
|
`create table reset_password (
|
||||||
|
token text not null PRIMARY KEY,
|
||||||
|
user_id uuid references users(id) on delete cascade unique,
|
||||||
|
expire timestamptz not null default now() + interval '30 minutes'
|
||||||
|
)`,
|
||||||
|
`create index "idx_reset_pw_expire" on "reset_password" ("expire")`,
|
||||||
|
`create table reset_twofa (
|
||||||
|
token text not null PRIMARY KEY,
|
||||||
|
user_id uuid references users(id) on delete cascade unique,
|
||||||
|
expire timestamptz not null default now() + interval '30 minutes'
|
||||||
|
)`,
|
||||||
|
`create index "idx_reset_twofa_expire" on "reset_twofa" ("expire")`,
|
||||||
|
`create table user_register_tokens (
|
||||||
|
token text not null PRIMARY KEY,
|
||||||
|
username text not null unique,
|
||||||
|
role role default 'user',
|
||||||
|
expire timestamptz not null default now() + interval '30 minutes'
|
||||||
|
)`,
|
||||||
|
// migrate values from customers which reference user_tokens for data persistence
|
||||||
|
`alter table customers add column sms_override_by_old text`,
|
||||||
|
`alter table customers add column id_card_data_override_by_old text`,
|
||||||
|
`alter table customers add column id_card_photo_override_by_old text`,
|
||||||
|
`alter table customers add column front_camera_override_by_old text`,
|
||||||
|
`alter table customers add column sanctions_override_by_old text`,
|
||||||
|
`alter table customers add column authorized_override_by_old text`,
|
||||||
|
`alter table customers add column us_ssn_override_by_old text`,
|
||||||
|
`update customers set sms_override_by_old=ut.name from user_tokens ut
|
||||||
|
where customers.sms_override_by=ut.token`,
|
||||||
|
`update customers set id_card_data_override_by_old=ut.name from user_tokens ut
|
||||||
|
where customers.id_card_data_override_by=ut.token`,
|
||||||
|
`update customers set id_card_photo_override_by_old=ut.name from user_tokens ut
|
||||||
|
where customers.id_card_photo_override_by=ut.token`,
|
||||||
|
`update customers set front_camera_override_by_old=ut.name from user_tokens ut
|
||||||
|
where customers.front_camera_override_by=ut.token`,
|
||||||
|
`update customers set sanctions_override_by_old=ut.name from user_tokens ut
|
||||||
|
where customers.sanctions_override_by=ut.token`,
|
||||||
|
`update customers set authorized_override_by_old=ut.name from user_tokens ut
|
||||||
|
where customers.authorized_override_by=ut.token`,
|
||||||
|
`update customers set us_ssn_override_by_old=ut.name from user_tokens ut
|
||||||
|
where customers.us_ssn_override_by=ut.token`,
|
||||||
|
`alter table customers drop column sms_override_by`,
|
||||||
|
`alter table customers drop column id_card_data_override_by`,
|
||||||
|
`alter table customers drop column id_card_photo_override_by`,
|
||||||
|
`alter table customers drop column front_camera_override_by`,
|
||||||
|
`alter table customers drop column sanctions_override_by`,
|
||||||
|
`alter table customers drop column authorized_override_by`,
|
||||||
|
`alter table customers drop column us_ssn_override_by`,
|
||||||
|
`alter table customers add column sms_override_by uuid references users(id)`,
|
||||||
|
`alter table customers add column id_card_data_override_by uuid references users(id)`,
|
||||||
|
`alter table customers add column id_card_photo_override_by uuid references users(id)`,
|
||||||
|
`alter table customers add column front_camera_override_by uuid references users(id)`,
|
||||||
|
`alter table customers add column sanctions_override_by uuid references users(id)`,
|
||||||
|
`alter table customers add column authorized_override_by uuid references users(id)`,
|
||||||
|
`alter table customers add column us_ssn_override_by uuid references users(id)`,
|
||||||
|
// migrate values from compliance_overrides which reference user_tokens for data persistence
|
||||||
|
`alter table compliance_overrides add column override_by_old text`,
|
||||||
|
`update compliance_overrides set override_by_old=ut.name from user_tokens ut
|
||||||
|
where compliance_overrides.override_by=ut.token`,
|
||||||
|
`alter table compliance_overrides drop column override_by`,
|
||||||
|
`alter table compliance_overrides add column override_by uuid references users(id)`,
|
||||||
|
`drop table if exists one_time_passes`,
|
||||||
|
`drop table if exists user_tokens`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
10549
new-lamassu-admin/package-lock.json
generated
10549
new-lamassu-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -32,6 +32,7 @@
|
||||||
"react-copy-to-clipboard": "^5.0.2",
|
"react-copy-to-clipboard": "^5.0.2",
|
||||||
"react-dom": "^16.10.2",
|
"react-dom": "^16.10.2",
|
||||||
"react-number-format": "^4.4.1",
|
"react-number-format": "^4.4.1",
|
||||||
|
"react-otp-input": "^2.3.0",
|
||||||
"react-router-dom": "5.1.2",
|
"react-router-dom": "5.1.2",
|
||||||
"react-use": "15.3.2",
|
"react-use": "15.3.2",
|
||||||
"react-virtualized": "^9.21.2",
|
"react-virtualized": "^9.21.2",
|
||||||
|
|
|
||||||
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Black.otf
Normal file
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Black.otf
Normal file
Binary file not shown.
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Bold.otf
Normal file
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Bold.otf
Normal file
Binary file not shown.
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Medium.otf
Normal file
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Medium.otf
Normal file
Binary file not shown.
|
|
@ -6,15 +6,19 @@ import styles from './Button.styles'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const ActionButton = memo(({ size = 'lg', children, className, ...props }) => {
|
const ActionButton = memo(
|
||||||
const classes = useStyles({ size })
|
({ size = 'lg', children, className, buttonClassName, ...props }) => {
|
||||||
return (
|
const classes = useStyles({ size })
|
||||||
<div className={classnames(className, classes.wrapper)}>
|
return (
|
||||||
<button className={classes.button} {...props}>
|
<div className={classnames(className, classes.wrapper)}>
|
||||||
{children}
|
<button
|
||||||
</button>
|
className={classnames(buttonClassName, classes.button)}
|
||||||
</div>
|
{...props}>
|
||||||
)
|
{children}
|
||||||
})
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export default ActionButton
|
export default ActionButton
|
||||||
|
|
|
||||||
40
new-lamassu-admin/src/components/inputs/base/CodeInput.js
Normal file
40
new-lamassu-admin/src/components/inputs/base/CodeInput.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { makeStyles } from '@material-ui/core'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import React from 'react'
|
||||||
|
import OtpInput from 'react-otp-input'
|
||||||
|
|
||||||
|
import styles from './CodeInput.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const CodeInput = ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
numInputs,
|
||||||
|
error,
|
||||||
|
inputStyle,
|
||||||
|
containerStyle,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OtpInput
|
||||||
|
id={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
numInputs={numInputs}
|
||||||
|
separator={<span> </span>}
|
||||||
|
containerStyle={classnames(containerStyle, classes.container)}
|
||||||
|
inputStyle={classnames(inputStyle, classes.input)}
|
||||||
|
focusStyle={classes.focus}
|
||||||
|
errorStyle={classes.error}
|
||||||
|
hasErrored={error}
|
||||||
|
isInputNum={true}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeInput
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {
|
||||||
|
fontPrimary,
|
||||||
|
primaryColor,
|
||||||
|
zircon,
|
||||||
|
errorColor
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
input: {
|
||||||
|
width: '3.5rem !important',
|
||||||
|
height: '5rem',
|
||||||
|
fontFamily: fontPrimary,
|
||||||
|
fontSize: 35,
|
||||||
|
color: primaryColor,
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: zircon,
|
||||||
|
borderRadius: '4px'
|
||||||
|
},
|
||||||
|
focus: {
|
||||||
|
color: primaryColor,
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: primaryColor,
|
||||||
|
borderRadius: '4px'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
borderColor: errorColor
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
justifyContent: 'space-evenly'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styles
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Autocomplete from './Autocomplete'
|
import Autocomplete from './Autocomplete'
|
||||||
import Checkbox from './Checkbox'
|
import Checkbox from './Checkbox'
|
||||||
|
import CodeInput from './CodeInput'
|
||||||
import NumberInput from './NumberInput'
|
import NumberInput from './NumberInput'
|
||||||
import RadioGroup from './RadioGroup'
|
import RadioGroup from './RadioGroup'
|
||||||
import SecretInput from './SecretInput'
|
import SecretInput from './SecretInput'
|
||||||
|
|
@ -8,6 +9,7 @@ import TextInput from './TextInput'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
CodeInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
Switch,
|
Switch,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Autocomplete from './base/Autocomplete'
|
import Autocomplete from './base/Autocomplete'
|
||||||
import Checkbox from './base/Checkbox'
|
import Checkbox from './base/Checkbox'
|
||||||
|
import CodeInput from './base/CodeInput'
|
||||||
import RadioGroup from './base/RadioGroup'
|
import RadioGroup from './base/RadioGroup'
|
||||||
import Select from './base/Select'
|
import Select from './base/Select'
|
||||||
import Switch from './base/Switch'
|
import Switch from './base/Switch'
|
||||||
|
|
@ -10,6 +11,7 @@ export {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
TextInput,
|
TextInput,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
CodeInput,
|
||||||
Switch,
|
Switch,
|
||||||
Select,
|
Select,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ const Subheader = ({ item, classes }) => {
|
||||||
|
|
||||||
const notNil = R.compose(R.not, R.isNil)
|
const notNil = R.compose(R.not, R.isNil)
|
||||||
|
|
||||||
const Header = memo(({ tree }) => {
|
const Header = memo(({ tree, user }) => {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const [notifButtonCoords, setNotifButtonCoords] = useState({ x: 0, y: 0 })
|
const [notifButtonCoords, setNotifButtonCoords] = useState({ x: 0, y: 0 })
|
||||||
|
|
@ -119,24 +119,35 @@ const Header = memo(({ tree }) => {
|
||||||
</div>
|
</div>
|
||||||
<nav className={classes.nav}>
|
<nav className={classes.nav}>
|
||||||
<ul className={classes.ul}>
|
<ul className={classes.ul}>
|
||||||
{tree.map((it, idx) => (
|
{tree.map((it, idx) => {
|
||||||
<NavLink
|
if (
|
||||||
key={idx}
|
!R.includes(
|
||||||
to={it.route || it.children[0].route}
|
user.role,
|
||||||
isActive={match => {
|
it.allowedRoles.map(v => {
|
||||||
if (!match) return false
|
return v.key
|
||||||
setActive(it)
|
})
|
||||||
return true
|
)
|
||||||
}}
|
)
|
||||||
className={classnames(classes.link, classes.whiteLink)}
|
return <></>
|
||||||
activeClassName={classes.activeLink}>
|
return (
|
||||||
<li className={classes.li}>
|
<NavLink
|
||||||
<span className={classes.forceSize} forcesize={it.label}>
|
key={idx}
|
||||||
{it.label}
|
to={it.route || it.children[0].route}
|
||||||
</span>
|
isActive={match => {
|
||||||
</li>
|
if (!match) return false
|
||||||
</NavLink>
|
setActive(it)
|
||||||
))}
|
return true
|
||||||
|
}}
|
||||||
|
className={classnames(classes.link, classes.whiteLink)}
|
||||||
|
activeClassName={classes.activeLink}>
|
||||||
|
<li className={classes.li}>
|
||||||
|
<span className={classes.forceSize} forcesize={it.label}>
|
||||||
|
{it.label}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div className={classes.actionButtonsContainer}>
|
<div className={classes.actionButtonsContainer}>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||||
import Grid from '@material-ui/core/Grid'
|
import Grid from '@material-ui/core/Grid'
|
||||||
import Slide from '@material-ui/core/Slide'
|
|
||||||
import {
|
import {
|
||||||
StylesProvider,
|
StylesProvider,
|
||||||
jssPreset,
|
jssPreset,
|
||||||
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, { createContext, useContext, useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
useLocation,
|
useLocation,
|
||||||
useHistory,
|
useHistory,
|
||||||
|
|
@ -17,15 +17,14 @@ import {
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
|
||||||
import AppContext from 'src/AppContext'
|
import AppContext from 'src/AppContext'
|
||||||
|
import Header from 'src/components/layout/Header'
|
||||||
import Sidebar from 'src/components/layout/Sidebar'
|
import Sidebar from 'src/components/layout/Sidebar'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import ApolloProvider from 'src/utils/apollo'
|
import ApolloProvider from 'src/pazuz/apollo/Provider'
|
||||||
|
import { tree, hasSidebar, Routes, getParent } from 'src/pazuz/routing/routes'
|
||||||
import Header from '../components/layout/Header'
|
import global from 'src/styling/global'
|
||||||
import { tree, hasSidebar, Routes, getParent } from '../routing/routes'
|
import theme from 'src/styling/theme'
|
||||||
import global from '../styling/global'
|
import { backgroundColor, mainWidth } from 'src/styling/variables'
|
||||||
import theme from '../styling/theme'
|
|
||||||
import { backgroundColor, mainWidth } from '../styling/variables'
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||||
|
|
@ -74,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
|
||||||
|
|
||||||
|
|
@ -93,20 +92,12 @@ 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 && (
|
||||||
<Slide
|
<TitleSection title={parent.title}></TitleSection>
|
||||||
direction="left"
|
|
||||||
in={true}
|
|
||||||
mountOnEnter
|
|
||||||
unmountOnExit
|
|
||||||
children={
|
|
||||||
<div>
|
|
||||||
<TitleSection title={parent.title}></TitleSection>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Grid container className={classes.grid}>
|
<Grid container className={classes.grid}>
|
||||||
|
|
@ -129,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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
115
new-lamassu-admin/src/pages/Authentication/Input2FAState.js
Normal file
115
new-lamassu-admin/src/pages/Authentication/Input2FAState.js
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import axios from 'axios'
|
||||||
|
import React, { useContext, useState } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { AppContext } from 'src/App'
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { CodeInput } from 'src/components/inputs/base'
|
||||||
|
import { H2, P } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from './Login.styles'
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const Input2FAState = ({
|
||||||
|
twoFAField,
|
||||||
|
onTwoFAChange,
|
||||||
|
clientField,
|
||||||
|
passwordField,
|
||||||
|
rememberMeField
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
const { setUserData } = useContext(AppContext)
|
||||||
|
|
||||||
|
const [invalidToken, setInvalidToken] = useState(false)
|
||||||
|
|
||||||
|
const handle2FAChange = value => {
|
||||||
|
onTwoFAChange(value)
|
||||||
|
setInvalidToken(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle2FA = () => {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${url}/api/login/2fa`,
|
||||||
|
data: {
|
||||||
|
username: clientField,
|
||||||
|
password: passwordField,
|
||||||
|
rememberMe: rememberMeField,
|
||||||
|
twoFACode: twoFAField
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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 = () => {
|
||||||
|
axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: `${url}/user-data`,
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 200) setUserData(res.data.user)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.status === 403) setUserData(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<H2 className={classes.info}>
|
||||||
|
Enter your two-factor authentication code
|
||||||
|
</H2>
|
||||||
|
<CodeInput
|
||||||
|
name="2fa"
|
||||||
|
value={twoFAField}
|
||||||
|
onChange={handle2FAChange}
|
||||||
|
numInputs={6}
|
||||||
|
error={invalidToken}
|
||||||
|
/>
|
||||||
|
<div className={classes.twofaFooter}>
|
||||||
|
{invalidToken && (
|
||||||
|
<P className={classes.errorMessage}>
|
||||||
|
Code is invalid. Please try again.
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handle2FA()
|
||||||
|
}}
|
||||||
|
buttonClassName={classes.loginButton}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input2FAState
|
||||||
30
new-lamassu-admin/src/pages/Authentication/Login.js
Normal file
30
new-lamassu-admin/src/pages/Authentication/Login.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { makeStyles, Grid } from '@material-ui/core'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import styles from './Login.styles'
|
||||||
|
import LoginCard from './LoginCard'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justify="center"
|
||||||
|
style={{ minHeight: '100vh' }}
|
||||||
|
className={classes.welcomeBackground}>
|
||||||
|
<Grid>
|
||||||
|
<LoginCard />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
121
new-lamassu-admin/src/pages/Authentication/Login.styles.js
Normal file
121
new-lamassu-admin/src/pages/Authentication/Login.styles.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import {
|
||||||
|
fontPrimary,
|
||||||
|
fontSecondary,
|
||||||
|
primaryColor,
|
||||||
|
backgroundColor,
|
||||||
|
errorColor
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
title: {
|
||||||
|
color: primaryColor,
|
||||||
|
fontFamily: fontPrimary,
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
paddingTop: 8
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 550
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
marginBottom: 25,
|
||||||
|
marginTop: -15
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
padding: '2.5em 4em',
|
||||||
|
width: 575,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
},
|
||||||
|
titleWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 30
|
||||||
|
},
|
||||||
|
rememberMeWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
transform: 'scale(1.5)',
|
||||||
|
marginRight: 25
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
transform: 'scale(2)',
|
||||||
|
marginRight: 5,
|
||||||
|
marginLeft: -5
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
marginTop: '10vh'
|
||||||
|
},
|
||||||
|
twofaFooter: {
|
||||||
|
marginTop: '6vh'
|
||||||
|
},
|
||||||
|
loginButton: {
|
||||||
|
display: 'block',
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
welcomeBackground: {
|
||||||
|
background: 'url(/wizard-background.svg) no-repeat center center fixed',
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
// filter: 'blur(4px)',
|
||||||
|
// pointerEvents: 'none',
|
||||||
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
position: 'relative',
|
||||||
|
left: '50%',
|
||||||
|
right: '50%',
|
||||||
|
marginLeft: '-50vw',
|
||||||
|
marginRight: '-50vw'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
fontFamily: fontSecondary,
|
||||||
|
marginBottom: '5vh'
|
||||||
|
},
|
||||||
|
info2: {
|
||||||
|
fontFamily: fontSecondary,
|
||||||
|
fontSize: '14px',
|
||||||
|
textAlign: 'justify'
|
||||||
|
},
|
||||||
|
infoWrapper: {
|
||||||
|
marginBottom: '3vh'
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
fontFamily: fontSecondary,
|
||||||
|
color: errorColor
|
||||||
|
},
|
||||||
|
qrCodeWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: '3vh'
|
||||||
|
},
|
||||||
|
secretWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
secretLabel: {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 550,
|
||||||
|
marginRight: 15
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 550,
|
||||||
|
marginRight: 35
|
||||||
|
},
|
||||||
|
hiddenSecret: {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 550,
|
||||||
|
marginRight: 35,
|
||||||
|
filter: 'blur(8px)'
|
||||||
|
},
|
||||||
|
confirm2FAInput: {
|
||||||
|
marginTop: 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styles
|
||||||
104
new-lamassu-admin/src/pages/Authentication/LoginCard.js
Normal file
104
new-lamassu-admin/src/pages/Authentication/LoginCard.js
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import Paper from '@material-ui/core/Paper'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { H2 } from 'src/components/typography'
|
||||||
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
|
||||||
|
import Input2FAState from './Input2FAState'
|
||||||
|
import styles from './Login.styles'
|
||||||
|
import LoginState from './LoginState'
|
||||||
|
import Setup2FAState from './Setup2FAState'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const STATES = {
|
||||||
|
LOGIN: 'Login',
|
||||||
|
SETUP_2FA: 'Setup 2FA',
|
||||||
|
INPUT_2FA: 'Input 2FA'
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginCard = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [twoFAField, setTwoFAField] = useState('')
|
||||||
|
const [clientField, setClientField] = useState('')
|
||||||
|
const [passwordField, setPasswordField] = useState('')
|
||||||
|
const [rememberMeField, setRememberMeField] = useState(false)
|
||||||
|
const [loginState, setLoginState] = useState(STATES.LOGIN)
|
||||||
|
|
||||||
|
const onClientChange = newValue => {
|
||||||
|
setClientField(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPasswordChange = newValue => {
|
||||||
|
setPasswordField(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRememberMeChange = newValue => {
|
||||||
|
setRememberMeField(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTwoFAChange = newValue => {
|
||||||
|
setTwoFAField(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoginState = newState => {
|
||||||
|
setLoginState(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderState = () => {
|
||||||
|
switch (loginState) {
|
||||||
|
case STATES.LOGIN:
|
||||||
|
return (
|
||||||
|
<LoginState
|
||||||
|
clientField={clientField}
|
||||||
|
onClientChange={onClientChange}
|
||||||
|
passwordField={passwordField}
|
||||||
|
onPasswordChange={onPasswordChange}
|
||||||
|
rememberMeField={rememberMeField}
|
||||||
|
onRememberMeChange={onRememberMeChange}
|
||||||
|
STATES={STATES}
|
||||||
|
handleLoginState={handleLoginState}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case STATES.INPUT_2FA:
|
||||||
|
return (
|
||||||
|
<Input2FAState
|
||||||
|
twoFAField={twoFAField}
|
||||||
|
onTwoFAChange={onTwoFAChange}
|
||||||
|
clientField={clientField}
|
||||||
|
passwordField={passwordField}
|
||||||
|
rememberMeField={rememberMeField}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case STATES.SETUP_2FA:
|
||||||
|
return (
|
||||||
|
<Setup2FAState
|
||||||
|
clientField={clientField}
|
||||||
|
passwordField={passwordField}
|
||||||
|
STATES={STATES}
|
||||||
|
handleLoginState={handleLoginState}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Paper elevation={1}>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<div className={classes.titleWrapper}>
|
||||||
|
<Logo className={classes.icon} />
|
||||||
|
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||||
|
</div>
|
||||||
|
{renderState()}
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginCard
|
||||||
130
new-lamassu-admin/src/pages/Authentication/LoginState.js
Normal file
130
new-lamassu-admin/src/pages/Authentication/LoginState.js
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import axios from 'axios'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { Checkbox, TextInput } from 'src/components/inputs/base'
|
||||||
|
import { Label2, P } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from './Login.styles'
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const LoginState = ({
|
||||||
|
clientField,
|
||||||
|
onClientChange,
|
||||||
|
passwordField,
|
||||||
|
onPasswordChange,
|
||||||
|
rememberMeField,
|
||||||
|
onRememberMeChange,
|
||||||
|
STATES,
|
||||||
|
handleLoginState
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [invalidLogin, setInvalidLogin] = useState(false)
|
||||||
|
|
||||||
|
const handleClientChange = event => {
|
||||||
|
onClientChange(event.target.value)
|
||||||
|
setInvalidLogin(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordChange = event => {
|
||||||
|
onPasswordChange(event.target.value)
|
||||||
|
setInvalidLogin(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRememberMeChange = () => {
|
||||||
|
onRememberMeChange(!rememberMeField)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${url}/api/login`,
|
||||||
|
data: {
|
||||||
|
username: clientField,
|
||||||
|
password: passwordField,
|
||||||
|
rememberMe: rememberMeField
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
withCredentials: true
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res) {
|
||||||
|
const status = res.status
|
||||||
|
const message = res.data.message
|
||||||
|
if (status === 200 && message === 'INPUT2FA')
|
||||||
|
handleLoginState(STATES.INPUT_2FA)
|
||||||
|
if (status === 200 && message === 'SETUP2FA')
|
||||||
|
handleLoginState(STATES.SETUP_2FA)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
if (err.response.status === 403) setInvalidLogin(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Label2 className={classes.inputLabel}>Client</Label2>
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
error={invalidLogin}
|
||||||
|
name="client-name"
|
||||||
|
autoFocus
|
||||||
|
id="client-name"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
onChange={handleClientChange}
|
||||||
|
value={clientField}
|
||||||
|
/>
|
||||||
|
<Label2 className={classes.inputLabel}>Password</Label2>
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
error={invalidLogin}
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
value={passwordField}
|
||||||
|
/>
|
||||||
|
<div className={classes.rememberMeWrapper}>
|
||||||
|
<Checkbox
|
||||||
|
className={classes.checkbox}
|
||||||
|
id="remember-me"
|
||||||
|
onChange={handleRememberMeChange}
|
||||||
|
value={rememberMeField}
|
||||||
|
/>
|
||||||
|
<Label2 className={classes.inputLabel}>Keep me logged in</Label2>
|
||||||
|
</div>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
{invalidLogin && (
|
||||||
|
<P className={classes.errorMessage}>
|
||||||
|
Invalid login/password combination.
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleLogin()
|
||||||
|
}}
|
||||||
|
buttonClassName={classes.loginButton}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginState
|
||||||
180
new-lamassu-admin/src/pages/Authentication/Register.js
Normal file
180
new-lamassu-admin/src/pages/Authentication/Register.js
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { makeStyles, Grid } from '@material-ui/core'
|
||||||
|
import Paper from '@material-ui/core/Paper'
|
||||||
|
import axios from 'axios'
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useLocation, useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { TextInput } from 'src/components/inputs/base'
|
||||||
|
import { H2, Label2, P } from 'src/components/typography'
|
||||||
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
|
||||||
|
import styles from './Login.styles'
|
||||||
|
|
||||||
|
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const Register = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
const query = useQuery()
|
||||||
|
const [passwordField, setPasswordField] = useState('')
|
||||||
|
const [confirmPasswordField, setConfirmPasswordField] = useState('')
|
||||||
|
const [invalidPassword, setInvalidPassword] = useState(false)
|
||||||
|
const [username, setUsername] = useState(null)
|
||||||
|
const [role, setRole] = useState(null)
|
||||||
|
const [isLoading, setLoading] = useState(true)
|
||||||
|
const [wasSuccessful, setSuccess] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validateQuery()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const validateQuery = () => {
|
||||||
|
axios({
|
||||||
|
url: `${url}/api/register?t=${query.get('t')}`,
|
||||||
|
method: 'GET',
|
||||||
|
options: {
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
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 = () => {
|
||||||
|
if (!isValidPassword()) return setInvalidPassword(true)
|
||||||
|
axios({
|
||||||
|
url: `${url}/api/register`,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
username: username,
|
||||||
|
password: passwordField,
|
||||||
|
role: role
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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 = () => {
|
||||||
|
return passwordField === confirmPasswordField
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordChange = event => {
|
||||||
|
setInvalidPassword(false)
|
||||||
|
setPasswordField(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmPasswordChange = event => {
|
||||||
|
setInvalidPassword(false)
|
||||||
|
setConfirmPasswordField(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justify="center"
|
||||||
|
style={{ minHeight: '100vh' }}
|
||||||
|
className={classes.welcomeBackground}>
|
||||||
|
<Grid>
|
||||||
|
<div>
|
||||||
|
<Paper elevation={1}>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<div className={classes.titleWrapper}>
|
||||||
|
<Logo className={classes.icon} />
|
||||||
|
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||||
|
</div>
|
||||||
|
{!isLoading && wasSuccessful && (
|
||||||
|
<>
|
||||||
|
<Label2 className={classes.inputLabel}>
|
||||||
|
Insert a password
|
||||||
|
</Label2>
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
error={invalidPassword}
|
||||||
|
name="new-password"
|
||||||
|
autoFocus
|
||||||
|
id="new-password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
value={passwordField}
|
||||||
|
/>
|
||||||
|
<Label2 className={classes.inputLabel}>
|
||||||
|
Confirm password
|
||||||
|
</Label2>
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
error={invalidPassword}
|
||||||
|
name="confirm-password"
|
||||||
|
id="confirm-password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
onChange={handleConfirmPasswordChange}
|
||||||
|
value={confirmPasswordField}
|
||||||
|
/>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
{invalidPassword && (
|
||||||
|
<P className={classes.errorMessage}>
|
||||||
|
Passwords do not match!
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleRegister()
|
||||||
|
}}
|
||||||
|
buttonClassName={classes.loginButton}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isLoading && !wasSuccessful && (
|
||||||
|
<>
|
||||||
|
<Label2 className={classes.inputLabel}>
|
||||||
|
Link has expired
|
||||||
|
</Label2>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Register
|
||||||
186
new-lamassu-admin/src/pages/Authentication/Reset2FA.js
Normal file
186
new-lamassu-admin/src/pages/Authentication/Reset2FA.js
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
import { makeStyles, Grid } from '@material-ui/core'
|
||||||
|
import Paper from '@material-ui/core/Paper'
|
||||||
|
import axios from 'axios'
|
||||||
|
import QRCode from 'qrcode.react'
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useLocation, useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { ActionButton, Button } from 'src/components/buttons'
|
||||||
|
import { CodeInput } from 'src/components/inputs/base'
|
||||||
|
import { H2, Label2, P } from 'src/components/typography'
|
||||||
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
import { primaryColor } from 'src/styling/variables'
|
||||||
|
|
||||||
|
import styles from './Login.styles'
|
||||||
|
|
||||||
|
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const Reset2FA = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
const query = useQuery()
|
||||||
|
const [userID, setUserID] = useState(null)
|
||||||
|
const [isLoading, setLoading] = useState(true)
|
||||||
|
const [wasSuccessful, setSuccess] = useState(false)
|
||||||
|
const [secret, setSecret] = useState(null)
|
||||||
|
const [otpauth, setOtpauth] = useState(null)
|
||||||
|
|
||||||
|
const [isShowing, setShowing] = useState(false)
|
||||||
|
const [invalidToken, setInvalidToken] = useState(false)
|
||||||
|
const [twoFAConfirmation, setTwoFAConfirmation] = useState('')
|
||||||
|
|
||||||
|
const handle2FAChange = value => {
|
||||||
|
setTwoFAConfirmation(value)
|
||||||
|
setInvalidToken(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validateQuery()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const validateQuery = () => {
|
||||||
|
axios({
|
||||||
|
url: `${url}/api/reset2fa?t=${query.get('t')}`,
|
||||||
|
method: 'GET',
|
||||||
|
options: {
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
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 = () => {
|
||||||
|
axios({
|
||||||
|
url: `${url}/api/update2fa`,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
userID: userID,
|
||||||
|
secret: secret,
|
||||||
|
code: twoFAConfirmation
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
setInvalidToken(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justify="center"
|
||||||
|
style={{ minHeight: '100vh' }}
|
||||||
|
className={classes.welcomeBackground}>
|
||||||
|
<Grid>
|
||||||
|
<div>
|
||||||
|
<Paper elevation={1}>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<div className={classes.titleWrapper}>
|
||||||
|
<Logo className={classes.icon} />
|
||||||
|
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||||
|
</div>
|
||||||
|
{!isLoading && wasSuccessful && (
|
||||||
|
<>
|
||||||
|
<div className={classes.infoWrapper}>
|
||||||
|
<Label2 className={classes.info2}>
|
||||||
|
To finish this process, please scan the following QR code
|
||||||
|
or insert the secret further below on an authentication
|
||||||
|
app of your choice, preferably Google Authenticator or
|
||||||
|
Authy.
|
||||||
|
</Label2>
|
||||||
|
</div>
|
||||||
|
<div className={classes.qrCodeWrapper}>
|
||||||
|
<QRCode size={240} fgColor={primaryColor} value={otpauth} />
|
||||||
|
</div>
|
||||||
|
<div className={classes.secretWrapper}>
|
||||||
|
<Label2 className={classes.secretLabel}>
|
||||||
|
Your secret:
|
||||||
|
</Label2>
|
||||||
|
<Label2
|
||||||
|
className={
|
||||||
|
isShowing ? classes.secret : classes.hiddenSecret
|
||||||
|
}>
|
||||||
|
{secret}
|
||||||
|
</Label2>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowing(!isShowing)
|
||||||
|
}}>
|
||||||
|
{isShowing ? 'Hide' : 'Show'}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<div className={classes.confirm2FAInput}>
|
||||||
|
<CodeInput
|
||||||
|
name="2fa"
|
||||||
|
value={twoFAConfirmation}
|
||||||
|
onChange={handle2FAChange}
|
||||||
|
numInputs={6}
|
||||||
|
error={invalidToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.twofaFooter}>
|
||||||
|
{invalidToken && (
|
||||||
|
<P className={classes.errorMessage}>
|
||||||
|
Code is invalid. Please try again.
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handle2FAReset()
|
||||||
|
}}
|
||||||
|
buttonClassName={classes.loginButton}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isLoading && !wasSuccessful && (
|
||||||
|
<>
|
||||||
|
<Label2 className={classes.inputLabel}>
|
||||||
|
Link has expired
|
||||||
|
</Label2>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reset2FA
|
||||||
177
new-lamassu-admin/src/pages/Authentication/ResetPassword.js
Normal file
177
new-lamassu-admin/src/pages/Authentication/ResetPassword.js
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { makeStyles, Grid } from '@material-ui/core'
|
||||||
|
import Paper from '@material-ui/core/Paper'
|
||||||
|
import axios from 'axios'
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useLocation, useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { TextInput } from 'src/components/inputs/base'
|
||||||
|
import { H2, Label2, P } from 'src/components/typography'
|
||||||
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
|
||||||
|
import styles from './Login.styles'
|
||||||
|
|
||||||
|
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const ResetPassword = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
const query = useQuery()
|
||||||
|
const [newPasswordField, setNewPasswordField] = useState('')
|
||||||
|
const [confirmPasswordField, setConfirmPasswordField] = useState('')
|
||||||
|
const [invalidPassword, setInvalidPassword] = useState(false)
|
||||||
|
const [userID, setUserID] = useState(null)
|
||||||
|
const [isLoading, setLoading] = useState(true)
|
||||||
|
const [wasSuccessful, setSuccess] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validateQuery()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const validateQuery = () => {
|
||||||
|
axios({
|
||||||
|
url: `${url}/api/resetpassword?t=${query.get('t')}`,
|
||||||
|
method: 'GET',
|
||||||
|
options: {
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
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 = () => {
|
||||||
|
if (!isValidPasswordChange()) return setInvalidPassword(true)
|
||||||
|
axios({
|
||||||
|
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 = () => {
|
||||||
|
return newPasswordField === confirmPasswordField
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewPasswordChange = event => {
|
||||||
|
setInvalidPassword(false)
|
||||||
|
setNewPasswordField(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmPasswordChange = event => {
|
||||||
|
setInvalidPassword(false)
|
||||||
|
setConfirmPasswordField(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justify="center"
|
||||||
|
style={{ minHeight: '100vh' }}
|
||||||
|
className={classes.welcomeBackground}>
|
||||||
|
<Grid>
|
||||||
|
<div>
|
||||||
|
<Paper elevation={1}>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<div className={classes.titleWrapper}>
|
||||||
|
<Logo className={classes.icon} />
|
||||||
|
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||||
|
</div>
|
||||||
|
{!isLoading && wasSuccessful && (
|
||||||
|
<>
|
||||||
|
<Label2 className={classes.inputLabel}>
|
||||||
|
Insert new password
|
||||||
|
</Label2>
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
error={invalidPassword}
|
||||||
|
name="new-password"
|
||||||
|
autoFocus
|
||||||
|
id="new-password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
onChange={handleNewPasswordChange}
|
||||||
|
value={newPasswordField}
|
||||||
|
/>
|
||||||
|
<Label2 className={classes.inputLabel}>
|
||||||
|
Confirm new password
|
||||||
|
</Label2>
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
error={invalidPassword}
|
||||||
|
name="confirm-password"
|
||||||
|
id="confirm-password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
onChange={handleConfirmPasswordChange}
|
||||||
|
value={confirmPasswordField}
|
||||||
|
/>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
{invalidPassword && (
|
||||||
|
<P className={classes.errorMessage}>
|
||||||
|
Passwords do not match!
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handlePasswordReset()
|
||||||
|
}}
|
||||||
|
buttonClassName={classes.loginButton}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isLoading && !wasSuccessful && (
|
||||||
|
<>
|
||||||
|
<Label2 className={classes.inputLabel}>
|
||||||
|
Link has expired
|
||||||
|
</Label2>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResetPassword
|
||||||
180
new-lamassu-admin/src/pages/Authentication/Setup2FAState.js
Normal file
180
new-lamassu-admin/src/pages/Authentication/Setup2FAState.js
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import axios from 'axios'
|
||||||
|
import QRCode from 'qrcode.react'
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
import { ActionButton, Button } from 'src/components/buttons'
|
||||||
|
import { CodeInput } from 'src/components/inputs/base'
|
||||||
|
import { Label2, P } from 'src/components/typography'
|
||||||
|
import { primaryColor } from 'src/styling/variables'
|
||||||
|
|
||||||
|
import styles from './Login.styles'
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const Setup2FAState = ({
|
||||||
|
clientField,
|
||||||
|
passwordField,
|
||||||
|
STATES,
|
||||||
|
handleLoginState
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [secret, setSecret] = useState(null)
|
||||||
|
const [otpauth, setOtpauth] = useState(null)
|
||||||
|
const [isShowing, setShowing] = useState(false)
|
||||||
|
|
||||||
|
const [invalidToken, setInvalidToken] = useState(false)
|
||||||
|
const [twoFAConfirmation, setTwoFAConfirmation] = useState('')
|
||||||
|
|
||||||
|
const handle2FAChange = value => {
|
||||||
|
setTwoFAConfirmation(value)
|
||||||
|
setInvalidToken(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
get2FASecret()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const get2FASecret = () => {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
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 = () => {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${url}/api/login/2fa/save`,
|
||||||
|
data: {
|
||||||
|
username: clientField,
|
||||||
|
password: passwordField,
|
||||||
|
secret: secret,
|
||||||
|
code: twoFAConfirmation
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
withCredentials: true
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) console.log(err)
|
||||||
|
if (res) {
|
||||||
|
const status = res.status
|
||||||
|
if (status === 200) handleLoginState(STATES.LOGIN)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
if (err.response.status === 403) {
|
||||||
|
setInvalidToken(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{secret && otpauth ? (
|
||||||
|
<>
|
||||||
|
<div className={classes.infoWrapper}>
|
||||||
|
<Label2 className={classes.info2}>
|
||||||
|
We detected that this account does not have its two-factor
|
||||||
|
authentication enabled. In order to protect the resources in the
|
||||||
|
system, a two-factor authentication is enforced.
|
||||||
|
</Label2>
|
||||||
|
<Label2 className={classes.info2}>
|
||||||
|
To finish this process, please scan the following QR code or
|
||||||
|
insert the secret further below on an authentication app of your
|
||||||
|
choice, preferably Google Authenticator or Authy.
|
||||||
|
</Label2>
|
||||||
|
</div>
|
||||||
|
<div className={classes.qrCodeWrapper}>
|
||||||
|
<QRCode size={240} fgColor={primaryColor} value={otpauth} />
|
||||||
|
</div>
|
||||||
|
<div className={classes.secretWrapper}>
|
||||||
|
<Label2 className={classes.secretLabel}>Your secret:</Label2>
|
||||||
|
<Label2
|
||||||
|
className={isShowing ? classes.secret : classes.hiddenSecret}>
|
||||||
|
{secret}
|
||||||
|
</Label2>
|
||||||
|
<ActionButton
|
||||||
|
disabled={!secret && !otpauth}
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowing(!isShowing)
|
||||||
|
}}>
|
||||||
|
{isShowing ? 'Hide' : 'Show'}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<div className={classes.confirm2FAInput}>
|
||||||
|
<CodeInput
|
||||||
|
name="2fa"
|
||||||
|
value={twoFAConfirmation}
|
||||||
|
onChange={handle2FAChange}
|
||||||
|
numInputs={6}
|
||||||
|
error={invalidToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.twofaFooter}>
|
||||||
|
{invalidToken && (
|
||||||
|
<P className={classes.errorMessage}>
|
||||||
|
Code is invalid. Please try again.
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
save2FASecret()
|
||||||
|
}}
|
||||||
|
buttonClassName={classes.loginButton}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// TODO: should maybe show a spinner here?
|
||||||
|
<div className={classes.twofaFooter}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
console.log('response should be arriving soon')
|
||||||
|
}}
|
||||||
|
buttonClassName={classes.loginButton}>
|
||||||
|
Generate Two Factor Authentication Secret
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Setup2FAState
|
||||||
|
|
@ -34,7 +34,12 @@ const GET_MACHINES = gql`
|
||||||
const NUM_LOG_RESULTS = 500
|
const NUM_LOG_RESULTS = 500
|
||||||
|
|
||||||
const GET_MACHINE_LOGS_CSV = gql`
|
const GET_MACHINE_LOGS_CSV = gql`
|
||||||
query MachineLogs($deviceId: ID!, $limit: Int, $from: Date, $until: Date) {
|
query MachineLogs(
|
||||||
|
$deviceId: ID!
|
||||||
|
$limit: Int
|
||||||
|
$from: DateTime
|
||||||
|
$until: DateTime
|
||||||
|
) {
|
||||||
machineLogsCsv(
|
machineLogsCsv(
|
||||||
deviceId: $deviceId
|
deviceId: $deviceId
|
||||||
limit: $limit
|
limit: $limit
|
||||||
|
|
@ -45,7 +50,12 @@ const GET_MACHINE_LOGS_CSV = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const GET_MACHINE_LOGS = gql`
|
const GET_MACHINE_LOGS = gql`
|
||||||
query MachineLogs($deviceId: ID!, $limit: Int, $from: Date, $until: Date) {
|
query MachineLogs(
|
||||||
|
$deviceId: ID!
|
||||||
|
$limit: Int
|
||||||
|
$from: DateTime
|
||||||
|
$until: DateTime
|
||||||
|
) {
|
||||||
machineLogs(
|
machineLogs(
|
||||||
deviceId: $deviceId
|
deviceId: $deviceId
|
||||||
limit: $limit
|
limit: $limit
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,13 @@ const formatDate = date => {
|
||||||
const NUM_LOG_RESULTS = 500
|
const NUM_LOG_RESULTS = 500
|
||||||
|
|
||||||
const GET_CSV = gql`
|
const GET_CSV = gql`
|
||||||
query ServerData($limit: Int, $from: Date, $until: Date) {
|
query ServerData($limit: Int, $from: DateTime, $until: DateTime) {
|
||||||
serverLogsCsv(limit: $limit, from: $from, until: $until)
|
serverLogsCsv(limit: $limit, from: $from, until: $until)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const GET_DATA = gql`
|
const GET_DATA = gql`
|
||||||
query ServerData($limit: Int, $from: Date, $until: Date) {
|
query ServerData($limit: Int, $from: DateTime, $until: DateTime) {
|
||||||
serverVersion
|
serverVersion
|
||||||
uptime {
|
uptime {
|
||||||
name
|
name
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import moment from 'moment'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React from 'react'
|
||||||
|
import parser from 'ua-parser-js'
|
||||||
|
|
||||||
|
import { IconButton } from 'src/components/buttons'
|
||||||
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||||
|
|
||||||
|
const GET_SESSIONS = gql`
|
||||||
|
query sessions {
|
||||||
|
sessions {
|
||||||
|
sid
|
||||||
|
sess
|
||||||
|
expire
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DELETE_SESSION = gql`
|
||||||
|
mutation deleteSession($sid: String!) {
|
||||||
|
deleteSession(sid: $sid) {
|
||||||
|
sid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const isLocalhost = ip => {
|
||||||
|
return ip === 'localhost' || ip === '::1' || ip === '127.0.0.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionManagement = () => {
|
||||||
|
const { data: tknResponse } = useQuery(GET_SESSIONS)
|
||||||
|
|
||||||
|
const [deleteSession] = useMutation(DELETE_SESSION, {
|
||||||
|
refetchQueries: () => ['sessions']
|
||||||
|
})
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
header: 'Login',
|
||||||
|
width: 207,
|
||||||
|
textAlign: 'left',
|
||||||
|
size: 'sm',
|
||||||
|
view: s => s.sess.user.username
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Last known use',
|
||||||
|
width: 305,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: s => {
|
||||||
|
const ua = parser(s.sess.ua)
|
||||||
|
return s.sess.ua
|
||||||
|
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
|
||||||
|
: `No Record`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Last known location',
|
||||||
|
width: 250,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: s => {
|
||||||
|
return isLocalhost(s.sess.ipAddress) ? 'This device' : s.sess.ipAddress
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Expiration date (UTC)',
|
||||||
|
width: 290,
|
||||||
|
textAlign: 'right',
|
||||||
|
size: 'sm',
|
||||||
|
view: s =>
|
||||||
|
`${moment.utc(s.expire).format('YYYY-MM-DD')} ${moment
|
||||||
|
.utc(s.expire)
|
||||||
|
.format('HH:mm:ss')}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
width: 80,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: s => (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
deleteSession({ variables: { sid: s.sid } })
|
||||||
|
}}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TitleSection title="Session Management" />
|
||||||
|
<DataTable elements={elements} data={R.path(['sessions'])(tknResponse)} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SessionManagement
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import moment from 'moment'
|
|
||||||
import * as R from 'ramda'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { IconButton } from 'src/components/buttons'
|
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
|
||||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
|
||||||
|
|
||||||
const GET_USER_TOKENS = gql`
|
|
||||||
query userTokens {
|
|
||||||
userTokens {
|
|
||||||
token
|
|
||||||
name
|
|
||||||
created
|
|
||||||
user_agent
|
|
||||||
ip_address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const REVOKE_USER_TOKEN = gql`
|
|
||||||
mutation revokeToken($token: String!) {
|
|
||||||
revokeToken(token: $token) {
|
|
||||||
token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Tokens = () => {
|
|
||||||
const { data: tknResponse } = useQuery(GET_USER_TOKENS)
|
|
||||||
|
|
||||||
const [revokeToken] = useMutation(REVOKE_USER_TOKEN, {
|
|
||||||
refetchQueries: () => ['userTokens']
|
|
||||||
})
|
|
||||||
|
|
||||||
const elements = [
|
|
||||||
{
|
|
||||||
header: 'Name',
|
|
||||||
width: 257,
|
|
||||||
textAlign: 'center',
|
|
||||||
size: 'sm',
|
|
||||||
view: t => t.name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Token',
|
|
||||||
width: 505,
|
|
||||||
textAlign: 'center',
|
|
||||||
size: 'sm',
|
|
||||||
view: t => t.token
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Date (UTC)',
|
|
||||||
width: 145,
|
|
||||||
textAlign: 'right',
|
|
||||||
size: 'sm',
|
|
||||||
view: t => moment.utc(t.created).format('YYYY-MM-DD')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Time (UTC)',
|
|
||||||
width: 145,
|
|
||||||
textAlign: 'right',
|
|
||||||
size: 'sm',
|
|
||||||
view: t => moment.utc(t.created).format('HH:mm:ss')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: '',
|
|
||||||
width: 80,
|
|
||||||
textAlign: 'center',
|
|
||||||
size: 'sm',
|
|
||||||
view: t => (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
revokeToken({ variables: { token: t.token } })
|
|
||||||
}}>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TitleSection title="Token Management" />
|
|
||||||
<DataTable
|
|
||||||
elements={elements}
|
|
||||||
data={R.path(['userTokens'])(tknResponse)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Tokens
|
|
||||||
|
|
@ -24,13 +24,13 @@ const useStyles = makeStyles(mainStyles)
|
||||||
const NUM_LOG_RESULTS = 1000
|
const NUM_LOG_RESULTS = 1000
|
||||||
|
|
||||||
const GET_TRANSACTIONS_CSV = gql`
|
const GET_TRANSACTIONS_CSV = gql`
|
||||||
query transactions($limit: Int, $from: Date, $until: Date) {
|
query transactions($limit: Int, $from: DateTime, $until: DateTime) {
|
||||||
transactionsCsv(limit: $limit, from: $from, until: $until)
|
transactionsCsv(limit: $limit, from: $from, until: $until)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const GET_TRANSACTIONS = gql`
|
const GET_TRANSACTIONS = gql`
|
||||||
query transactions($limit: Int, $from: Date, $until: Date) {
|
query transactions($limit: Int, $from: DateTime, $until: DateTime) {
|
||||||
transactions(limit: $limit, from: $from, until: $until) {
|
transactions(limit: $limit, from: $from, until: $until) {
|
||||||
id
|
id
|
||||||
txClass
|
txClass
|
||||||
|
|
|
||||||
386
new-lamassu-admin/src/pages/UserManagement/UserManagement.js
Normal file
386
new-lamassu-admin/src/pages/UserManagement/UserManagement.js
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
|
import { makeStyles, Box, Chip } from '@material-ui/core'
|
||||||
|
import axios from 'axios'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
// import moment from 'moment'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { useState, useContext } from 'react'
|
||||||
|
// import parser from 'ua-parser-js'
|
||||||
|
|
||||||
|
import { AppContext } from 'src/App'
|
||||||
|
import { Link /*, IconButton */ } from 'src/components/buttons'
|
||||||
|
import { Switch } from 'src/components/inputs'
|
||||||
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
// import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||||
|
|
||||||
|
import styles from './UserManagement.styles'
|
||||||
|
import ChangeRoleModal from './modals/ChangeRoleModal'
|
||||||
|
import CreateUserModal from './modals/CreateUserModal'
|
||||||
|
// import DeleteUserModal from './modals/DeleteUserModal'
|
||||||
|
import EnableUserModal from './modals/EnableUserModal'
|
||||||
|
import Input2FAModal from './modals/Input2FAModal'
|
||||||
|
import Reset2FAModal from './modals/Reset2FAModal'
|
||||||
|
import ResetPasswordModal from './modals/ResetPasswordModal'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const GET_USERS = gql`
|
||||||
|
query users {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
role
|
||||||
|
enabled
|
||||||
|
last_accessed
|
||||||
|
last_accessed_from
|
||||||
|
last_accessed_address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/* const DELETE_USERS = gql`
|
||||||
|
mutation deleteUser($id: ID!) {
|
||||||
|
deleteUser(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
` */
|
||||||
|
|
||||||
|
const CHANGE_USER_ROLE = gql`
|
||||||
|
mutation changeUserRole($id: ID!, $newRole: String!) {
|
||||||
|
changeUserRole(id: $id, newRole: $newRole) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TOGGLE_USER_ENABLE = gql`
|
||||||
|
mutation toggleUserEnable($id: ID!) {
|
||||||
|
toggleUserEnable(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Users = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const { userData } = useContext(AppContext)
|
||||||
|
|
||||||
|
const { data: userResponse } = useQuery(GET_USERS)
|
||||||
|
|
||||||
|
/* const [deleteUser] = useMutation(DELETE_USERS, {
|
||||||
|
refetchQueries: () => ['users']
|
||||||
|
}) */
|
||||||
|
|
||||||
|
const [changeUserRole] = useMutation(CHANGE_USER_ROLE, {
|
||||||
|
refetchQueries: () => ['users']
|
||||||
|
})
|
||||||
|
|
||||||
|
const [toggleUserEnable] = useMutation(TOGGLE_USER_ENABLE, {
|
||||||
|
refetchQueries: () => ['users']
|
||||||
|
})
|
||||||
|
|
||||||
|
const [userInfo, setUserInfo] = useState(null)
|
||||||
|
|
||||||
|
const [showCreateUserModal, setShowCreateUserModal] = useState(false)
|
||||||
|
const toggleCreateUserModal = () =>
|
||||||
|
setShowCreateUserModal(!showCreateUserModal)
|
||||||
|
|
||||||
|
const [showResetPasswordModal, setShowResetPasswordModal] = useState(false)
|
||||||
|
const [resetPasswordUrl, setResetPasswordUrl] = useState('')
|
||||||
|
const toggleResetPasswordModal = () =>
|
||||||
|
setShowResetPasswordModal(!showResetPasswordModal)
|
||||||
|
|
||||||
|
const [showReset2FAModal, setShowReset2FAModal] = useState(false)
|
||||||
|
const [reset2FAUrl, setReset2FAUrl] = useState('')
|
||||||
|
const toggleReset2FAModal = () => setShowReset2FAModal(!showReset2FAModal)
|
||||||
|
|
||||||
|
const [showRoleModal, setShowRoleModal] = useState(false)
|
||||||
|
const toggleRoleModal = () =>
|
||||||
|
setShowRoleModal(!showRoleModal)
|
||||||
|
|
||||||
|
const [showEnableUserModal, setShowEnableUserModal] = useState(false)
|
||||||
|
const toggleEnableUserModal = () =>
|
||||||
|
setShowEnableUserModal(!showEnableUserModal)
|
||||||
|
|
||||||
|
/* const [showDeleteUserModal, setShowDeleteUserModal] = useState(false)
|
||||||
|
const toggleDeleteUserModal = () =>
|
||||||
|
setShowDeleteUserModal(!showDeleteUserModal) */
|
||||||
|
|
||||||
|
const [showInputConfirmModal, setShowInputConfirmModal] = useState(false)
|
||||||
|
const toggleInputConfirmModal = () =>
|
||||||
|
setShowInputConfirmModal(!showInputConfirmModal)
|
||||||
|
|
||||||
|
const [action, setAction] = useState(null)
|
||||||
|
|
||||||
|
const requestNewPassword = userID => {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${url}/api/resetpassword`,
|
||||||
|
data: {
|
||||||
|
userID: userID
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res) {
|
||||||
|
const status = res.status
|
||||||
|
if (status === 200) {
|
||||||
|
const token = res.data.token
|
||||||
|
setResetPasswordUrl(
|
||||||
|
`https://localhost:3001/resetpassword?t=${token.token}`
|
||||||
|
)
|
||||||
|
toggleResetPasswordModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err) console.log('error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestNew2FA = userID => {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${url}/api/reset2fa`,
|
||||||
|
data: {
|
||||||
|
userID: userID
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res) {
|
||||||
|
const status = res.status
|
||||||
|
if (status === 200) {
|
||||||
|
const token = res.data.token
|
||||||
|
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
|
||||||
|
toggleReset2FAModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err) console.log('error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
header: 'Login',
|
||||||
|
width: 257,
|
||||||
|
textAlign: 'left',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => {
|
||||||
|
if (userData.id === u.id)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{u.username}
|
||||||
|
<Chip size="small" label="You" className={classes.chip} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
return u.username
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Role',
|
||||||
|
width: 105,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => {
|
||||||
|
switch (u.role) {
|
||||||
|
case 'user':
|
||||||
|
return 'Regular'
|
||||||
|
case 'superuser':
|
||||||
|
return 'Superuser'
|
||||||
|
default:
|
||||||
|
return u.role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
width: 80,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => (
|
||||||
|
<Switch
|
||||||
|
disabled={userData.id === u.id}
|
||||||
|
checked={u.role === 'superuser'}
|
||||||
|
onClick={() => {
|
||||||
|
setUserInfo(u)
|
||||||
|
toggleRoleModal()
|
||||||
|
}}
|
||||||
|
value={u.role === 'superuser'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
width: 25,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Actions',
|
||||||
|
width: 565,
|
||||||
|
textAlign: 'left',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label="Reset password"
|
||||||
|
className={classes.actionChip}
|
||||||
|
onClick={() => {
|
||||||
|
setUserInfo(u)
|
||||||
|
if(u.role === 'superuser') {
|
||||||
|
setAction(() => requestNewPassword.bind(null, u.id))
|
||||||
|
toggleInputConfirmModal()
|
||||||
|
} else {
|
||||||
|
requestNewPassword(u.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label="Reset 2FA"
|
||||||
|
className={classes.actionChip}
|
||||||
|
onClick={() => {
|
||||||
|
setUserInfo(u)
|
||||||
|
if(u.role === 'superuser') {
|
||||||
|
setAction(() => requestNew2FA.bind(null, u.id))
|
||||||
|
toggleInputConfirmModal()
|
||||||
|
} else {
|
||||||
|
requestNew2FA(u.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* {
|
||||||
|
header: 'Actions',
|
||||||
|
width: 535,
|
||||||
|
textAlign: 'left',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => {
|
||||||
|
const ua = parser(u.last_accessed_from)
|
||||||
|
return u.last_accessed_from
|
||||||
|
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
|
||||||
|
: `No Record`
|
||||||
|
}
|
||||||
|
}, */
|
||||||
|
{
|
||||||
|
header: 'Enabled',
|
||||||
|
width: 100,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => (
|
||||||
|
<Switch
|
||||||
|
disabled={userData.id === u.id}
|
||||||
|
checked={u.enabled}
|
||||||
|
onClick={() => {
|
||||||
|
setUserInfo(u)
|
||||||
|
toggleEnableUserModal()
|
||||||
|
}}
|
||||||
|
value={u.enabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}/* ,
|
||||||
|
{
|
||||||
|
header: 'Delete',
|
||||||
|
width: 100,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: u => (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setUserInfo(u)
|
||||||
|
toggleDeleteUserModal()
|
||||||
|
}}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
} */
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TitleSection title="User Management" />
|
||||||
|
<Box
|
||||||
|
marginBottom={3}
|
||||||
|
marginTop={-5}
|
||||||
|
className={classes.tableWidth}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="flex-end">
|
||||||
|
<Link color="primary" onClick={toggleCreateUserModal}>
|
||||||
|
Add new user
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<DataTable elements={elements} data={R.path(['users'])(userResponse)} />
|
||||||
|
<CreateUserModal
|
||||||
|
showModal={showCreateUserModal}
|
||||||
|
toggleModal={toggleCreateUserModal}
|
||||||
|
/>
|
||||||
|
<ResetPasswordModal
|
||||||
|
showModal={showResetPasswordModal}
|
||||||
|
toggleModal={toggleResetPasswordModal}
|
||||||
|
resetPasswordURL={resetPasswordUrl}
|
||||||
|
user={userInfo}
|
||||||
|
/>
|
||||||
|
<Reset2FAModal
|
||||||
|
showModal={showReset2FAModal}
|
||||||
|
toggleModal={toggleReset2FAModal}
|
||||||
|
reset2FAURL={reset2FAUrl}
|
||||||
|
user={userInfo}
|
||||||
|
/>
|
||||||
|
<ChangeRoleModal
|
||||||
|
showModal={showRoleModal}
|
||||||
|
toggleModal={toggleRoleModal}
|
||||||
|
user={userInfo}
|
||||||
|
confirm={changeUserRole}
|
||||||
|
inputConfirmToggle={toggleInputConfirmModal}
|
||||||
|
setAction={setAction}
|
||||||
|
/>
|
||||||
|
<EnableUserModal
|
||||||
|
showModal={showEnableUserModal}
|
||||||
|
toggleModal={toggleEnableUserModal}
|
||||||
|
user={userInfo}
|
||||||
|
confirm={toggleUserEnable}
|
||||||
|
inputConfirmToggle={toggleInputConfirmModal}
|
||||||
|
setAction={setAction}
|
||||||
|
/>
|
||||||
|
{/* <DeleteUserModal
|
||||||
|
showModal={showDeleteUserModal}
|
||||||
|
toggleModal={toggleDeleteUserModal}
|
||||||
|
user={userInfo}
|
||||||
|
confirm={deleteUser}
|
||||||
|
inputConfirmToggle={toggleInputConfirmModal}
|
||||||
|
setAction={setAction}
|
||||||
|
/> */}
|
||||||
|
<Input2FAModal
|
||||||
|
showModal={showInputConfirmModal}
|
||||||
|
toggleModal={toggleInputConfirmModal}
|
||||||
|
action={action}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Users
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import {
|
||||||
|
spacer,
|
||||||
|
fontPrimary,
|
||||||
|
fontSecondary,
|
||||||
|
primaryColor,
|
||||||
|
subheaderColor,
|
||||||
|
errorColor
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
footer: {
|
||||||
|
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
marginTop: -5,
|
||||||
|
color: primaryColor,
|
||||||
|
fontFamily: fontPrimary
|
||||||
|
},
|
||||||
|
modalLabel1: {
|
||||||
|
marginTop: 20
|
||||||
|
},
|
||||||
|
modalLabel2: {
|
||||||
|
marginTop: 40
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
color: primaryColor,
|
||||||
|
fontFamily: fontPrimary,
|
||||||
|
fontSize: 24,
|
||||||
|
marginLeft: 8,
|
||||||
|
marginTop: 15
|
||||||
|
},
|
||||||
|
tableWidth: {
|
||||||
|
width: 1132
|
||||||
|
},
|
||||||
|
radioGroup: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
width: 500
|
||||||
|
},
|
||||||
|
radioLabel: {
|
||||||
|
width: 150,
|
||||||
|
height: 48
|
||||||
|
},
|
||||||
|
copyToClipboard: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
paddingTop: 6,
|
||||||
|
paddingLeft: 15,
|
||||||
|
marginRight: -11
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
backgroundColor: subheaderColor,
|
||||||
|
fontFamily: fontPrimary,
|
||||||
|
marginLeft: 15,
|
||||||
|
marginTop: -5
|
||||||
|
},
|
||||||
|
actionChip: {
|
||||||
|
backgroundColor: subheaderColor,
|
||||||
|
marginRight: 15,
|
||||||
|
marginTop: -5
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
fontFamily: fontSecondary,
|
||||||
|
textAlign: 'justify'
|
||||||
|
},
|
||||||
|
addressWrapper: {
|
||||||
|
backgroundColor: subheaderColor,
|
||||||
|
marginTop: 8
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
margin: `${spacer * 1.5}px ${spacer * 3}px`
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
fontFamily: fontSecondary,
|
||||||
|
color: errorColor
|
||||||
|
},
|
||||||
|
codeContainer: {
|
||||||
|
marginTop: 15,
|
||||||
|
marginBottom: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styles
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { H2, Info3 } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from '../UserManagement.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const ChangeRoleModal = ({
|
||||||
|
showModal,
|
||||||
|
toggleModal,
|
||||||
|
user,
|
||||||
|
confirm,
|
||||||
|
inputConfirmToggle,
|
||||||
|
setAction
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={275}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
<H2 className={classes.modalTitle}>Change {user.username}'s role?</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
You are about to alter {user.username}'s role. This will change this
|
||||||
|
user's permission to access certain resources.
|
||||||
|
</Info3>
|
||||||
|
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setAction(() =>
|
||||||
|
confirm.bind(null, {
|
||||||
|
variables: {
|
||||||
|
id: user.id,
|
||||||
|
newRole: user.role === 'superuser' ? 'user' : 'superuser'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
inputConfirmToggle()
|
||||||
|
handleClose()
|
||||||
|
}}>
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangeRoleModal
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import axios from 'axios'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { RadioGroup } from 'src/components/inputs'
|
||||||
|
import { TextInput } from 'src/components/inputs/base'
|
||||||
|
import { H1, H2, H3, Info3, Mono } from 'src/components/typography'
|
||||||
|
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||||
|
|
||||||
|
import styles from '../UserManagement.styles'
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const CreateUserModal = ({ showModal, toggleModal }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [usernameField, setUsernameField] = useState('')
|
||||||
|
const [roleField, setRoleField] = useState('')
|
||||||
|
const [createUserURL, setCreateUserURL] = useState(null)
|
||||||
|
const [invalidUser, setInvalidUser] = useState(false)
|
||||||
|
|
||||||
|
const radioOptions = [
|
||||||
|
{
|
||||||
|
code: 'user',
|
||||||
|
display: 'Regular user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'superuser',
|
||||||
|
display: 'Superuser'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleUsernameChange = event => {
|
||||||
|
if (event.target.value === '') {
|
||||||
|
setInvalidUser(false)
|
||||||
|
}
|
||||||
|
setUsernameField(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRoleChange = event => {
|
||||||
|
setRoleField(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setUsernameField('')
|
||||||
|
setRoleField('')
|
||||||
|
setInvalidUser(false)
|
||||||
|
setCreateUserURL(null)
|
||||||
|
toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateUser = () => {
|
||||||
|
const username = usernameField.trim()
|
||||||
|
|
||||||
|
if (username === '') {
|
||||||
|
setInvalidUser(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${url}/api/createuser`,
|
||||||
|
data: {
|
||||||
|
username: username,
|
||||||
|
role: roleField
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res) {
|
||||||
|
const status = res.status
|
||||||
|
const message = res.data.message
|
||||||
|
if (status === 200 && message) setInvalidUser(true)
|
||||||
|
if (status === 200 && !message) {
|
||||||
|
const token = res.data.token
|
||||||
|
setCreateUserURL(`https://localhost:3001/register?t=${token.token}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err) console.log('error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal && !createUserURL && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={400}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
<H1 className={classes.modalTitle}>Create new user</H1>
|
||||||
|
<H3 className={classes.modalLabel1}>User login</H3>
|
||||||
|
<TextInput
|
||||||
|
error={invalidUser}
|
||||||
|
name="username"
|
||||||
|
autoFocus
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
width={338}
|
||||||
|
onChange={handleUsernameChange}
|
||||||
|
value={usernameField}
|
||||||
|
/>
|
||||||
|
<H3 className={classes.modalLabel2}>Role</H3>
|
||||||
|
<RadioGroup
|
||||||
|
name="userrole"
|
||||||
|
value={roleField}
|
||||||
|
options={radioOptions}
|
||||||
|
onChange={handleRoleChange}
|
||||||
|
className={classes.radioGroup}
|
||||||
|
labelClassName={classes.radioLabel}
|
||||||
|
/>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
<Button onClick={handleCreateUser}>Finish</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{showModal && createUserURL && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={215}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
<H2 className={classes.modalTitle}>Creating {usernameField}...</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
Safely share this link with {usernameField} to finish the
|
||||||
|
registration process.
|
||||||
|
</Info3>
|
||||||
|
<div className={classes.addressWrapper}>
|
||||||
|
<Mono className={classes.address}>
|
||||||
|
<strong>
|
||||||
|
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||||
|
{createUserURL}
|
||||||
|
</CopyToClipboard>
|
||||||
|
</strong>
|
||||||
|
</Mono>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateUserModal
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { H2, Info3 } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from '../UserManagement.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const DeleteUserModal = ({
|
||||||
|
showModal,
|
||||||
|
toggleModal,
|
||||||
|
user,
|
||||||
|
confirm,
|
||||||
|
inputConfirmToggle,
|
||||||
|
setAction
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={275}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
<H2 className={classes.modalTitle}>Delete {user.username}?</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
You are about to delete {user.username}. This will remove existent
|
||||||
|
sessions and revoke this user's permissions to access the system.
|
||||||
|
</Info3>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
This is a <b>PERMANENT</b> operation. Do you wish to proceed?
|
||||||
|
</Info3>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (user.role === 'superuser') {
|
||||||
|
setAction(() =>
|
||||||
|
confirm.bind(null, {
|
||||||
|
variables: {
|
||||||
|
id: user.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
inputConfirmToggle()
|
||||||
|
} else {
|
||||||
|
confirm({
|
||||||
|
variables: {
|
||||||
|
id: user.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
handleClose()
|
||||||
|
}}>
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeleteUserModal
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { H2, Info3 } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from '../UserManagement.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const EnableUserModal = ({
|
||||||
|
showModal,
|
||||||
|
toggleModal,
|
||||||
|
user,
|
||||||
|
confirm,
|
||||||
|
inputConfirmToggle,
|
||||||
|
setAction
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={275}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
{!user.enabled && (
|
||||||
|
<>
|
||||||
|
<H2 className={classes.modalTitle}>Enable {user.username}?</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
You are about to enable {user.username} into the system,
|
||||||
|
activating previous eligible sessions and grant permissions to
|
||||||
|
access the system.
|
||||||
|
</Info3>
|
||||||
|
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{user.enabled && (
|
||||||
|
<>
|
||||||
|
<H2 className={classes.modalTitle}>Disable {user.username}?</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
You are about to disable {user.username} from the system,
|
||||||
|
deactivating previous eligible sessions and removing permissions
|
||||||
|
to access the system.
|
||||||
|
</Info3>
|
||||||
|
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className={classes.footer}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (user.role === 'superuser') {
|
||||||
|
setAction(() =>
|
||||||
|
confirm.bind(null, {
|
||||||
|
variables: {
|
||||||
|
id: user.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
inputConfirmToggle()
|
||||||
|
} else {
|
||||||
|
confirm({
|
||||||
|
variables: {
|
||||||
|
id: user.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
handleClose()
|
||||||
|
}}>
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnableUserModal
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import axios from 'axios'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { CodeInput } from 'src/components/inputs/base'
|
||||||
|
import { H2, Info3, P } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from '../UserManagement.styles'
|
||||||
|
|
||||||
|
const url =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [twoFACode, setTwoFACode] = useState('')
|
||||||
|
const [invalidCode, setInvalidCode] = useState(false)
|
||||||
|
|
||||||
|
const handleCodeChange = value => {
|
||||||
|
setTwoFACode(value)
|
||||||
|
setInvalidCode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setTwoFACode('')
|
||||||
|
setInvalidCode(false)
|
||||||
|
toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleActionConfirm = () => {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${url}/api/confirm2fa`,
|
||||||
|
data: {
|
||||||
|
code: twoFACode
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res, err) => {
|
||||||
|
if (err) return
|
||||||
|
if (res) {
|
||||||
|
const status = res.status
|
||||||
|
if (status === 200) {
|
||||||
|
action()
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
const errStatus = err.response.status
|
||||||
|
if (errStatus === 401) setInvalidCode(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={400}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
<H2 className={classes.modalTitle}>Confirm action</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
Please confirm this action by placing your two-factor authentication
|
||||||
|
code below.
|
||||||
|
</Info3>
|
||||||
|
<CodeInput
|
||||||
|
name="2fa"
|
||||||
|
value={twoFACode}
|
||||||
|
onChange={handleCodeChange}
|
||||||
|
numInputs={6}
|
||||||
|
error={invalidCode}
|
||||||
|
containerStyle={classes.codeContainer}
|
||||||
|
shouldAutoFocus
|
||||||
|
/>
|
||||||
|
{invalidCode && (
|
||||||
|
<P className={classes.errorMessage}>
|
||||||
|
Code is invalid. Please try again.
|
||||||
|
</P>
|
||||||
|
)}
|
||||||
|
<div className={classes.footer}>
|
||||||
|
<Button onClick={handleActionConfirm}>Finish</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input2FAModal
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { H2, Info3, Mono } from 'src/components/typography'
|
||||||
|
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||||
|
|
||||||
|
import styles from '../UserManagement.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const Reset2FAModal = ({ showModal, toggleModal, reset2FAURL, user }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={215}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
<H2 className={classes.modalTitle}>Reset 2FA for {user.username}</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
Safely share this link with {user.username} for a two-factor
|
||||||
|
authentication reset.
|
||||||
|
</Info3>
|
||||||
|
<div className={classes.addressWrapper}>
|
||||||
|
<Mono className={classes.address}>
|
||||||
|
<strong>
|
||||||
|
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||||
|
{reset2FAURL}
|
||||||
|
</CopyToClipboard>
|
||||||
|
</strong>
|
||||||
|
</Mono>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reset2FAModal
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { H2, Info3, Mono } from 'src/components/typography'
|
||||||
|
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||||
|
|
||||||
|
import styles from '../UserManagement.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const ResetPasswordModal = ({
|
||||||
|
showModal,
|
||||||
|
toggleModal,
|
||||||
|
resetPasswordURL,
|
||||||
|
user
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
toggleModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal && (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={600}
|
||||||
|
height={215}
|
||||||
|
handleClose={handleClose}
|
||||||
|
open={true}>
|
||||||
|
<H2 className={classes.modalTitle}>
|
||||||
|
Reset password for {user.username}
|
||||||
|
</H2>
|
||||||
|
<Info3 className={classes.info}>
|
||||||
|
Safely share this link with {user.username} for a password reset.
|
||||||
|
</Info3>
|
||||||
|
<div className={classes.addressWrapper}>
|
||||||
|
<Mono className={classes.address}>
|
||||||
|
<strong>
|
||||||
|
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||||
|
{resetPasswordURL}
|
||||||
|
</CopyToClipboard>
|
||||||
|
</strong>
|
||||||
|
</Mono>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResetPasswordModal
|
||||||
|
|
@ -1,27 +1,14 @@
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Route, Redirect } from 'react-router-dom'
|
import { Route, Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
const isAuthenticated = () => {
|
import { AppContext } from 'src/App'
|
||||||
return localStorage.getItem('loggedIn')
|
|
||||||
}
|
|
||||||
|
|
||||||
const PrivateRoute = ({ children, ...rest }) => {
|
import { isLoggedIn } from './utils'
|
||||||
return (
|
|
||||||
<Route
|
const PrivateRoute = ({ ...rest }) => {
|
||||||
{...rest}
|
const { userData } = useContext(AppContext)
|
||||||
render={({ location }) =>
|
|
||||||
isAuthenticated() ? (
|
return isLoggedIn(userData) ? <Route {...rest} /> : <Redirect to="/login" />
|
||||||
children
|
|
||||||
) : (
|
|
||||||
<Redirect
|
|
||||||
to={{
|
|
||||||
pathname: '/login'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PrivateRoute
|
export default PrivateRoute
|
||||||
|
|
|
||||||
25
new-lamassu-admin/src/routing/PublicRoute.js
Normal file
25
new-lamassu-admin/src/routing/PublicRoute.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React, { useContext } from 'react'
|
||||||
|
import { Route, Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { AppContext } from 'src/App'
|
||||||
|
|
||||||
|
import { isLoggedIn } from './utils'
|
||||||
|
|
||||||
|
const PublicRoute = ({ component: Component, restricted, ...rest }) => {
|
||||||
|
const { userData } = useContext(AppContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
{...rest}
|
||||||
|
render={props =>
|
||||||
|
isLoggedIn(userData) && restricted ? (
|
||||||
|
<Redirect to="/" />
|
||||||
|
) : (
|
||||||
|
<Component {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublicRoute
|
||||||
|
|
@ -13,7 +13,11 @@ import {
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
|
||||||
import AppContext from 'src/AppContext'
|
import AppContext from 'src/AppContext'
|
||||||
import AuthRegister from 'src/pages/AuthRegister'
|
// import AuthRegister from 'src/pages/AuthRegister'
|
||||||
|
import Login from 'src/pages/Authentication/Login'
|
||||||
|
import Register from 'src/pages/Authentication/Register'
|
||||||
|
import Reset2FA from 'src/pages/Authentication/Reset2FA'
|
||||||
|
import ResetPassword from 'src/pages/Authentication/ResetPassword'
|
||||||
import Blacklist from 'src/pages/Blacklist'
|
import Blacklist from 'src/pages/Blacklist'
|
||||||
import Cashout from 'src/pages/Cashout'
|
import Cashout from 'src/pages/Cashout'
|
||||||
import Commissions from 'src/pages/Commissions'
|
import Commissions from 'src/pages/Commissions'
|
||||||
|
|
@ -34,13 +38,18 @@ 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 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'
|
||||||
|
import UserManagement from 'src/pages/UserManagement/UserManagement'
|
||||||
import WalletSettings from 'src/pages/Wallet/Wallet'
|
import WalletSettings from 'src/pages/Wallet/Wallet'
|
||||||
import Wizard from 'src/pages/Wizard'
|
import Wizard from 'src/pages/Wizard'
|
||||||
import { namespaces } from 'src/utils/config'
|
import { namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
|
import PrivateRoute from './PrivateRoute'
|
||||||
|
import PublicRoute from './PublicRoute'
|
||||||
|
import { ROLES } from './utils'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
wrapper: {
|
wrapper: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -55,12 +64,14 @@ const tree = [
|
||||||
key: 'transactions',
|
key: 'transactions',
|
||||||
label: 'Transactions',
|
label: 'Transactions',
|
||||||
route: '/transactions',
|
route: '/transactions',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Transactions
|
component: Transactions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'maintenance',
|
key: 'maintenance',
|
||||||
label: 'Maintenance',
|
label: 'Maintenance',
|
||||||
route: '/maintenance',
|
route: '/maintenance',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
get component() {
|
get component() {
|
||||||
return () => <Redirect to={this.children[0].route} />
|
return () => <Redirect to={this.children[0].route} />
|
||||||
},
|
},
|
||||||
|
|
@ -69,30 +80,35 @@ const tree = [
|
||||||
key: 'cash_cassettes',
|
key: 'cash_cassettes',
|
||||||
label: 'Cash Cassettes',
|
label: 'Cash Cassettes',
|
||||||
route: '/maintenance/cash-cassettes',
|
route: '/maintenance/cash-cassettes',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: CashCassettes
|
component: CashCassettes
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'funding',
|
key: 'funding',
|
||||||
label: 'Funding',
|
label: 'Funding',
|
||||||
route: '/maintenance/funding',
|
route: '/maintenance/funding',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Funding
|
component: Funding
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'logs',
|
key: 'logs',
|
||||||
label: 'Machine Logs',
|
label: 'Machine Logs',
|
||||||
route: '/maintenance/logs',
|
route: '/maintenance/logs',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: MachineLogs
|
component: MachineLogs
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'machine-status',
|
key: 'machine-status',
|
||||||
label: 'Machine Status',
|
label: 'Machine Status',
|
||||||
route: '/maintenance/machine-status',
|
route: '/maintenance/machine-status',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: MachineStatus
|
component: MachineStatus
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'server-logs',
|
key: 'server-logs',
|
||||||
label: 'Server',
|
label: 'Server',
|
||||||
route: '/maintenance/server-logs',
|
route: '/maintenance/server-logs',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: ServerLogs
|
component: ServerLogs
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -101,6 +117,7 @@ const tree = [
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
route: '/settings',
|
route: '/settings',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
get component() {
|
get component() {
|
||||||
return () => <Redirect to={this.children[0].route} />
|
return () => <Redirect to={this.children[0].route} />
|
||||||
},
|
},
|
||||||
|
|
@ -109,36 +126,42 @@ const tree = [
|
||||||
key: namespaces.COMMISSIONS,
|
key: namespaces.COMMISSIONS,
|
||||||
label: 'Commissions',
|
label: 'Commissions',
|
||||||
route: '/settings/commissions',
|
route: '/settings/commissions',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Commissions
|
component: Commissions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: namespaces.LOCALE,
|
key: namespaces.LOCALE,
|
||||||
label: 'Locales',
|
label: 'Locales',
|
||||||
route: '/settings/locale',
|
route: '/settings/locale',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Locales
|
component: Locales
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: namespaces.CASH_OUT,
|
key: namespaces.CASH_OUT,
|
||||||
label: 'Cash-out',
|
label: 'Cash-out',
|
||||||
route: '/settings/cash-out',
|
route: '/settings/cash-out',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Cashout
|
component: Cashout
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: namespaces.NOTIFICATIONS,
|
key: namespaces.NOTIFICATIONS,
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
route: '/settings/notifications',
|
route: '/settings/notifications',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Notifications
|
component: Notifications
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'services',
|
key: 'services',
|
||||||
label: '3rd party services',
|
label: '3rd party services',
|
||||||
route: '/settings/3rd-party-services',
|
route: '/settings/3rd-party-services',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Services
|
component: Services
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: namespaces.WALLETS,
|
key: namespaces.WALLETS,
|
||||||
label: 'Wallet',
|
label: 'Wallet',
|
||||||
route: '/settings/wallet-settings',
|
route: '/settings/wallet-settings',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: WalletSettings
|
component: WalletSettings
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -146,6 +169,7 @@ const tree = [
|
||||||
label: 'Operator Info',
|
label: 'Operator Info',
|
||||||
route: '/settings/operator-info',
|
route: '/settings/operator-info',
|
||||||
title: 'Operator Information',
|
title: 'Operator Information',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
get component() {
|
get component() {
|
||||||
return () => (
|
return () => (
|
||||||
<Redirect
|
<Redirect
|
||||||
|
|
@ -161,24 +185,28 @@ const tree = [
|
||||||
key: 'contact-info',
|
key: 'contact-info',
|
||||||
label: 'Contact information',
|
label: 'Contact information',
|
||||||
route: '/settings/operator-info/contact-info',
|
route: '/settings/operator-info/contact-info',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: ContactInfo
|
component: ContactInfo
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'receipt-printing',
|
key: 'receipt-printing',
|
||||||
label: 'Receipt',
|
label: 'Receipt',
|
||||||
route: '/settings/operator-info/receipt-printing',
|
route: '/settings/operator-info/receipt-printing',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: ReceiptPrinting
|
component: ReceiptPrinting
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'coin-atm-radar',
|
key: 'coin-atm-radar',
|
||||||
label: 'Coin ATM Radar',
|
label: 'Coin ATM Radar',
|
||||||
route: '/settings/operator-info/coin-atm-radar',
|
route: '/settings/operator-info/coin-atm-radar',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: CoinAtmRadar
|
component: CoinAtmRadar
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'terms-conditions',
|
key: 'terms-conditions',
|
||||||
label: 'Terms & Conditions',
|
label: 'Terms & Conditions',
|
||||||
route: '/settings/operator-info/terms-conditions',
|
route: '/settings/operator-info/terms-conditions',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: TermsConditions
|
component: TermsConditions
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -189,6 +217,7 @@ const tree = [
|
||||||
key: 'compliance',
|
key: 'compliance',
|
||||||
label: 'Compliance',
|
label: 'Compliance',
|
||||||
route: '/compliance',
|
route: '/compliance',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
get component() {
|
get component() {
|
||||||
return () => <Redirect to={this.children[0].route} />
|
return () => <Redirect to={this.children[0].route} />
|
||||||
},
|
},
|
||||||
|
|
@ -197,18 +226,21 @@ const tree = [
|
||||||
key: 'triggers',
|
key: 'triggers',
|
||||||
label: 'Triggers',
|
label: 'Triggers',
|
||||||
route: '/compliance/triggers',
|
route: '/compliance/triggers',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Triggers
|
component: Triggers
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'customers',
|
key: 'customers',
|
||||||
label: 'Customers',
|
label: 'Customers',
|
||||||
route: '/compliance/customers',
|
route: '/compliance/customers',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Customers
|
component: Customers
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'blacklist',
|
key: 'blacklist',
|
||||||
label: 'Blacklist',
|
label: 'Blacklist',
|
||||||
route: '/compliance/blacklist',
|
route: '/compliance/blacklist',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: Blacklist
|
component: Blacklist
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -220,9 +252,35 @@ const tree = [
|
||||||
{
|
{
|
||||||
key: 'customer',
|
key: 'customer',
|
||||||
route: '/compliance/customer/:id',
|
route: '/compliance/customer/:id',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: CustomerProfile
|
component: CustomerProfile
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system',
|
||||||
|
label: 'System',
|
||||||
|
route: '/system',
|
||||||
|
allowedRoles: [ROLES.SUPERUSER],
|
||||||
|
get component() {
|
||||||
|
return () => <Redirect to={this.children[0].route} />
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'user-management',
|
||||||
|
label: 'User Management',
|
||||||
|
route: '/system/user-management',
|
||||||
|
allowedRoles: [ROLES.SUPERUSER],
|
||||||
|
component: UserManagement
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'session-management',
|
||||||
|
label: 'Session Management',
|
||||||
|
route: '/system/session-management',
|
||||||
|
allowedRoles: [ROLES.SUPERUSER],
|
||||||
|
component: SessionManagement
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
// key: 'system',
|
// key: 'system',
|
||||||
|
|
@ -276,13 +334,32 @@ const Routes = () => {
|
||||||
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const { wizardTested, userData } = useContext(AppContext)
|
||||||
|
|
||||||
const { wizardTested } = useContext(AppContext)
|
const dontTriggerPages = [
|
||||||
|
'/404',
|
||||||
const dontTriggerPages = ['/404', '/register', '/wizard']
|
'/register',
|
||||||
|
'/wizard',
|
||||||
|
'/login',
|
||||||
|
'/register',
|
||||||
|
'/resetpassword',
|
||||||
|
'/reset2fa'
|
||||||
|
]
|
||||||
|
|
||||||
if (!wizardTested && !R.contains(location.pathname)(dontTriggerPages)) {
|
if (!wizardTested && !R.contains(location.pathname)(dontTriggerPages)) {
|
||||||
history.push('/wizard')
|
history.push('/wizard')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFilteredRoutes = () => {
|
||||||
|
if (!userData) return []
|
||||||
|
|
||||||
|
return flattened.filter(value => {
|
||||||
|
const keys = value.allowedRoles.map(v => {
|
||||||
|
return v.key
|
||||||
|
})
|
||||||
|
return R.includes(userData.role, keys)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const Transition = location.state ? Slide : Fade
|
const Transition = location.state ? Slide : Fade
|
||||||
|
|
@ -300,10 +377,10 @@ const Routes = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
<PrivateRoute exact path="/">
|
||||||
<Redirect to={{ pathname: '/dashboard' }} />
|
<Redirect to={{ pathname: '/transactions' }} />
|
||||||
</Route>
|
</PrivateRoute>
|
||||||
<Route path={'/dashboard'}>
|
<PrivateRoute path={'/dashboard'}>
|
||||||
<Transition
|
<Transition
|
||||||
className={classes.wrapper}
|
className={classes.wrapper}
|
||||||
{...transitionProps}
|
{...transitionProps}
|
||||||
|
|
@ -316,12 +393,15 @@ const Routes = () => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</PrivateRoute>
|
||||||
<Route path="/machines" component={Machines} />
|
<PrivateRoute path="/machines" component={Machines} />
|
||||||
<Route path="/wizard" component={Wizard} />
|
<PrivateRoute path="/wizard" component={Wizard} />
|
||||||
<Route path="/register" component={AuthRegister} />
|
<Route path="/register" component={Register} />
|
||||||
|
<PublicRoute path="/login" restricted component={Login} />
|
||||||
|
<Route path="/resetpassword" component={ResetPassword} />
|
||||||
|
<Route path="/reset2fa" component={Reset2FA} />
|
||||||
{/* <Route path="/configmigration" component={ConfigMigration} /> */}
|
{/* <Route path="/configmigration" component={ConfigMigration} /> */}
|
||||||
{flattened.map(({ route, component: Page, key }) => (
|
{getFilteredRoutes().map(({ route, component: Page, key }) => (
|
||||||
<Route path={route} key={key}>
|
<Route path={route} key={key}>
|
||||||
<Transition
|
<Transition
|
||||||
className={classes.wrapper}
|
className={classes.wrapper}
|
||||||
|
|
@ -331,7 +411,9 @@ const Routes = () => {
|
||||||
unmountOnExit
|
unmountOnExit
|
||||||
children={
|
children={
|
||||||
<div className={classes.wrapper}>
|
<div className={classes.wrapper}>
|
||||||
<Page name={key} />
|
<PrivateRoute path={route} key={key}>
|
||||||
|
<Page name={key} />
|
||||||
|
</PrivateRoute>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
8
new-lamassu-admin/src/routing/utils.js
Normal file
8
new-lamassu-admin/src/routing/utils.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const isLoggedIn = userData => {
|
||||||
|
return userData
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROLES = {
|
||||||
|
USER: { key: 'user', value: '0' },
|
||||||
|
SUPERUSER: { key: 'superuser', value: '1' }
|
||||||
|
}
|
||||||
|
|
@ -4,20 +4,23 @@ import { ApolloClient } from 'apollo-client'
|
||||||
import { ApolloLink } from 'apollo-link'
|
import { ApolloLink } from 'apollo-link'
|
||||||
import { onError } from 'apollo-link-error'
|
import { onError } from 'apollo-link-error'
|
||||||
import { HttpLink } from 'apollo-link-http'
|
import { HttpLink } from 'apollo-link-http'
|
||||||
import React 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'
|
||||||
|
|
||||||
const URI =
|
const URI =
|
||||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
const getClient = (history, location) =>
|
const getClient = (history, location, setUserData) =>
|
||||||
new ApolloClient({
|
new ApolloClient({
|
||||||
link: ApolloLink.from([
|
link: ApolloLink.from([
|
||||||
onError(({ graphQLErrors, networkError }) => {
|
onError(({ graphQLErrors, networkError }) => {
|
||||||
if (graphQLErrors)
|
if (graphQLErrors)
|
||||||
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
||||||
if (extensions?.code === 'UNAUTHENTICATED') {
|
if (extensions?.code === 'UNAUTHENTICATED') {
|
||||||
if (location.pathname !== '/404') history.push('/404')
|
setUserData(null)
|
||||||
|
if (location.pathname !== '/login') history.push('/login')
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||||
|
|
@ -49,7 +52,9 @@ const getClient = (history, location) =>
|
||||||
const Provider = ({ children }) => {
|
const Provider = ({ children }) => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const client = getClient(history, location)
|
const { setUserData } = useContext(AppContext)
|
||||||
|
const client = getClient(history, location, setUserData)
|
||||||
|
|
||||||
return <ApolloProvider client={client}>{children}</ApolloProvider>
|
return <ApolloProvider client={client}>{children}</ApolloProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
208
package-lock.json
generated
208
package-lock.json
generated
|
|
@ -1936,6 +1936,48 @@
|
||||||
"fastq": "^1.6.0"
|
"fastq": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@otplib/core": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA=="
|
||||||
|
},
|
||||||
|
"@otplib/plugin-crypto": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
|
||||||
|
"requires": {
|
||||||
|
"@otplib/core": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@otplib/plugin-thirty-two": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
|
||||||
|
"requires": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"thirty-two": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@otplib/preset-default": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
|
||||||
|
"requires": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/plugin-crypto": "^12.0.1",
|
||||||
|
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@otplib/preset-v11": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
|
||||||
|
"requires": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/plugin-crypto": "^12.0.1",
|
||||||
|
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@protobufjs/aspromise": {
|
"@protobufjs/aspromise": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
|
|
@ -2300,6 +2342,34 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/pg": {
|
||||||
|
"version": "7.14.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.14.9.tgz",
|
||||||
|
"integrity": "sha512-ThTOEwOvYM++zRSGiajRqKyTQboCEJE2VI+30d93WX94sQ7CnrcJ7CICT9oC+QD8Co9JTYJkKEfEXSb5DjUOFA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"pg-types": "^2.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"requires": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/prettier": {
|
"@types/prettier": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.6.tgz",
|
||||||
|
|
@ -3817,6 +3887,22 @@
|
||||||
"cashaddrjs": "^0.3.3"
|
"cashaddrjs": "^0.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"bcrypt": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==",
|
||||||
|
"requires": {
|
||||||
|
"node-addon-api": "^3.0.0",
|
||||||
|
"node-pre-gyp": "0.15.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"bcrypt-pbkdf": {
|
"bcrypt-pbkdf": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
|
|
@ -5642,6 +5728,68 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"connect-pg-simple": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-bwDp/gKyRtyz0V5Vxy3SATSxItWBK/wDhaacncC79+q1B1VB8SQ49AlVaQCM+XxmIO29cWX4cvsFj65mD2qrzA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/pg": "^7.14.4",
|
||||||
|
"pg": "^8.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-writer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
|
||||||
|
},
|
||||||
|
"packet-reader": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
|
||||||
|
},
|
||||||
|
"pg": {
|
||||||
|
"version": "8.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz",
|
||||||
|
"integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==",
|
||||||
|
"requires": {
|
||||||
|
"buffer-writer": "2.0.0",
|
||||||
|
"packet-reader": "1.0.0",
|
||||||
|
"pg-connection-string": "^2.4.0",
|
||||||
|
"pg-pool": "^3.2.2",
|
||||||
|
"pg-protocol": "^1.4.0",
|
||||||
|
"pg-types": "^2.1.0",
|
||||||
|
"pgpass": "1.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pg-connection-string": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ=="
|
||||||
|
},
|
||||||
|
"pg-pool": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA=="
|
||||||
|
},
|
||||||
|
"pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"requires": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||||
|
|
@ -7458,6 +7606,33 @@
|
||||||
"defaults": "^1.0.3"
|
"defaults": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"express-session": {
|
||||||
|
"version": "1.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz",
|
||||||
|
"integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==",
|
||||||
|
"requires": {
|
||||||
|
"cookie": "0.4.0",
|
||||||
|
"cookie-signature": "1.0.6",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"on-headers": "~1.0.2",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"safe-buffer": "5.2.0",
|
||||||
|
"uid-safe": "~2.1.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"express-ws": {
|
"express-ws": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-3.0.0.tgz",
|
||||||
|
|
@ -12674,6 +12849,16 @@
|
||||||
"os-tmpdir": "^1.0.0"
|
"os-tmpdir": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"otplib": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
|
||||||
|
"requires": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/preset-default": "^12.0.1",
|
||||||
|
"@otplib/preset-v11": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"p-cancelable": {
|
"p-cancelable": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
|
||||||
|
|
@ -13098,6 +13283,11 @@
|
||||||
"spex": "~2.0.2"
|
"spex": "~2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pg-protocol": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA=="
|
||||||
|
},
|
||||||
"pg-types": {
|
"pg-types": {
|
||||||
"version": "1.13.0",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.13.0.tgz",
|
||||||
|
|
@ -13725,6 +13915,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||||
},
|
},
|
||||||
|
"random-bytes": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
|
|
@ -16389,6 +16584,11 @@
|
||||||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"thirty-two": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-TKL//AKlEpDSdEueP1V2k8prYno="
|
||||||
|
},
|
||||||
"throat": {
|
"throat": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
||||||
|
|
@ -16745,6 +16945,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz",
|
||||||
"integrity": "sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q=="
|
"integrity": "sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q=="
|
||||||
},
|
},
|
||||||
|
"uid-safe": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||||
|
"requires": {
|
||||||
|
"random-bytes": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ultron": {
|
"ultron": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"axios": "^0.16.1",
|
"axios": "^0.16.1",
|
||||||
"base-x": "^3.0.2",
|
"base-x": "^3.0.2",
|
||||||
"bchaddrjs": "^0.3.0",
|
"bchaddrjs": "^0.3.0",
|
||||||
|
"bcrypt": "^5.0.0",
|
||||||
"bignumber.js": "^4.1.0",
|
"bignumber.js": "^4.1.0",
|
||||||
"bip39": "^2.3.1",
|
"bip39": "^2.3.1",
|
||||||
"bitcoind-rpc": "^0.7.0",
|
"bitcoind-rpc": "^0.7.0",
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
"body-parser": "^1.15.1",
|
"body-parser": "^1.15.1",
|
||||||
"coinbase": "^2.0.6",
|
"coinbase": "^2.0.6",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
|
"connect-pg-simple": "^6.2.1",
|
||||||
"console-log-level": "^1.4.0",
|
"console-log-level": "^1.4.0",
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|
@ -31,6 +33,7 @@
|
||||||
"express": "^4.15.4",
|
"express": "^4.15.4",
|
||||||
"express-limiter": "^1.6.0",
|
"express-limiter": "^1.6.0",
|
||||||
"express-rate-limit": "^2.9.0",
|
"express-rate-limit": "^2.9.0",
|
||||||
|
"express-session": "^1.17.1",
|
||||||
"express-ws": "^3.0.0",
|
"express-ws": "^3.0.0",
|
||||||
"futoin-hkdf": "^1.0.2",
|
"futoin-hkdf": "^1.0.2",
|
||||||
"got": "^7.1.0",
|
"got": "^7.1.0",
|
||||||
|
|
@ -59,6 +62,7 @@
|
||||||
"ndjson": "^1.5.0",
|
"ndjson": "^1.5.0",
|
||||||
"nocache": "^2.1.0",
|
"nocache": "^2.1.0",
|
||||||
"numeral": "^2.0.3",
|
"numeral": "^2.0.3",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"p-each-series": "^1.0.0",
|
"p-each-series": "^1.0.0",
|
||||||
"p-retry": "^4.4.0",
|
"p-retry": "^4.4.0",
|
||||||
"pg-native": "^3.0.0",
|
"pg-native": "^3.0.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue