From d295acc2614c556aeb85a4b0956f8b7cb1956a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 14 Dec 2020 17:33:47 +0000 Subject: [PATCH] fix: email verification and UX fix: remove annotations fix: styles fix: move directives from schema chore: rework auth routes feat: start graphql schema modularization feat: start directives rework fix: directive cycle fix: directive resolve fix: schema auth directive feat: migrate auth routes to gql fix: apollo client fix: migrate forms to formik refactor: user resolver chore: final touches on auth components fix: routes --- lib/new-admin/admin-server.js | 17 +- lib/new-admin/graphql/directives/auth.js | 40 +++ lib/new-admin/graphql/directives/index.js | 3 + .../graphql/modules/authentication.js | 220 +++++++++++++++ lib/new-admin/graphql/resolvers/index.js | 2 + .../graphql/resolvers/users.resolver.js | 35 +++ lib/new-admin/graphql/types/index.js | 2 + lib/new-admin/graphql/types/users.type.js | 77 +++++ lib/new-admin/routes/auth.js | 266 +----------------- new-lamassu-admin/src/lamassu/App.js | 2 +- .../src/pages/Authentication/Input2FAState.js | 116 ++++---- .../src/pages/Authentication/Login.js | 1 - .../src/pages/Authentication/Login.styles.js | 12 +- .../src/pages/Authentication/LoginCard.js | 3 - .../src/pages/Authentication/LoginState.js | 204 +++++++------- .../src/pages/Authentication/Register.js | 236 ++++++++-------- .../src/pages/Authentication/Reset2FA.js | 131 +++++---- .../src/pages/Authentication/ResetPassword.js | 231 ++++++++------- .../src/pages/Authentication/Setup2FAState.js | 146 ++++------ .../src/pages/Transactions/CopyToClipboard.js | 3 +- .../pages/UserManagement/UserManagement.js | 195 +++++-------- .../UserManagement/UserManagement.styles.js | 42 ++- .../UserManagement/modals/ChangeRoleModal.js | 19 +- .../UserManagement/modals/CreateUserModal.js | 172 +++++------ .../UserManagement/modals/DeleteUserModal.js | 15 +- .../UserManagement/modals/EnableUserModal.js | 27 +- .../UserManagement/modals/Input2FAModal.js | 87 +++--- .../UserManagement/modals/Reset2FAModal.js | 19 +- .../modals/ResetPasswordModal.js | 19 +- new-lamassu-admin/src/pazuz/App.js | 59 +++- new-lamassu-admin/src/routing/routes.js | 8 +- new-lamassu-admin/src/utils/apollo.js | 2 +- package-lock.json | 47 +++- 33 files changed, 1319 insertions(+), 1139 deletions(-) create mode 100644 lib/new-admin/graphql/directives/auth.js create mode 100644 lib/new-admin/graphql/directives/index.js create mode 100644 lib/new-admin/graphql/modules/authentication.js create mode 100644 lib/new-admin/graphql/resolvers/users.resolver.js create mode 100644 lib/new-admin/graphql/types/users.type.js diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index 236ab444..42c3c89f 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -21,7 +21,9 @@ const options = require('../options') const db = require('../db') const users = require('../users') -const { typeDefs, resolvers, AuthDirective, SuperuserDirective } = require('./graphql/schema') +const authRouter = require('./routes/auth') +const { AuthDirective } = require('./graphql/directives') +const { typeDefs, resolvers } = require('./graphql/schema') const devMode = require('minimist')(process.argv.slice(2)).dev const idPhotoCardBasedir = _.get('idPhotoCardDir', options) @@ -64,8 +66,7 @@ const apolloServer = new ApolloServer({ typeDefs, resolvers, schemaDirectives: { - auth: AuthDirective, - superuser: SuperuserDirective + auth: AuthDirective }, playground: false, introspection: false, @@ -74,7 +75,8 @@ const apolloServer = new ApolloServer({ return error }, context: async ({ req }) => { - if (!req.session.user) throw new AuthenticationError('Authentication failed') + if (!req.session.user) return { req } + const user = await users.verifyAndUpdateUser( req.session.user.id, req.headers['user-agent'] || 'Unknown', @@ -87,7 +89,8 @@ const apolloServer = new ApolloServer({ req.session.lastUsed = new Date(Date.now()).toISOString() req.session.user.id = user.id req.session.user.role = user.role - return { req: { ...req } } + + return { req } } }) @@ -104,9 +107,7 @@ app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' }) app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false })) app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false })) -app.use('/api', register) - -require('./routes/auth')(app) +app.use(authRouter) // Everything not on graphql or api/register is redirected to the front-end app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html'))) diff --git a/lib/new-admin/graphql/directives/auth.js b/lib/new-admin/graphql/directives/auth.js new file mode 100644 index 00000000..d4bbacf9 --- /dev/null +++ b/lib/new-admin/graphql/directives/auth.js @@ -0,0 +1,40 @@ +const _ = require('lodash/fp') + +const { SchemaDirectiveVisitor, AuthenticationError } = require('apollo-server-express') +const { defaultFieldResolver } = require('graphql') + +class AuthDirective extends SchemaDirectiveVisitor { + visitObject (type) { + this.ensureFieldsWrapped(type) + type._requiredAuthRole = this.args.requires + } + + visitFieldDefinition (field, details) { + this.ensureFieldsWrapped(details.objectType) + field._requiredAuthRole = this.args.requires + } + + ensureFieldsWrapped (objectType) { + if (objectType._authFieldsWrapped) return + objectType._authFieldsWrapped = true + + const fields = objectType.getFields() + + _.forEach(fieldName => { + const field = fields[fieldName] + const { resolve = defaultFieldResolver } = field + + field.resolve = function (root, args, context, info) { + const requiredRoles = field._requiredAuthRole ? field._requiredAuthRole : objectType._requiredAuthRole + if (!requiredRoles) return resolve.apply(this, [root, args, context, info]) + + const user = context.req.session.user + if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) throw new AuthenticationError('You do not have permission to access this resource!') + + return resolve.apply(this, [root, args, context, info]) + } + }, _.keys(fields)) + } +} + +module.exports = AuthDirective diff --git a/lib/new-admin/graphql/directives/index.js b/lib/new-admin/graphql/directives/index.js new file mode 100644 index 00000000..f2bbcf42 --- /dev/null +++ b/lib/new-admin/graphql/directives/index.js @@ -0,0 +1,3 @@ +const AuthDirective = require('./auth') + +module.exports = { AuthDirective } diff --git a/lib/new-admin/graphql/modules/authentication.js b/lib/new-admin/graphql/modules/authentication.js new file mode 100644 index 00000000..1b68ee36 --- /dev/null +++ b/lib/new-admin/graphql/modules/authentication.js @@ -0,0 +1,220 @@ +const otplib = require('otplib') +const bcrypt = require('bcrypt') + +const loginHelper = require('../../services/login') +const T = require('../../../time') +const users = require('../../../users') +const sessionManager = require('../../../session-manager') + +const REMEMBER_ME_AGE = 90 * T.day + +async function authenticateUser (username, password) { + const hashedPassword = await loginHelper.checkUser(username) + if (!hashedPassword) return null + + const isMatch = await bcrypt.compare(password, hashedPassword) + if (!isMatch) return null + + const user = await loginHelper.validateUser(username, hashedPassword) + if (!user) return null + return user +} + +const getUserData = context => { + const lidCookie = context.req.cookies && context.req.cookies.lid + if (!lidCookie) return null + + const user = context.req.session.user + return user +} + +const get2FASecret = (username, password) => { + return authenticateUser(username, password).then(user => { + if (!user) return null + + const secret = otplib.authenticator.generateSecret() + const otpauth = otplib.authenticator.keyuri(username, 'Lamassu Industries', secret) + return { secret, otpauth } + }) +} + +const confirm2FA = (codeArg, context) => { + const code = codeArg + const requestingUser = context.req.session.user + + if (!requestingUser) return false + + return users.get2FASecret(requestingUser.id).then(user => { + const secret = user.twofa_code + const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret }) + + if (!isCodeValid) return false + return true + }) +} + +const validateRegisterLink = token => { + if (!token) return null + return users.validateUserRegistrationToken(token) + .then(r => { + if (!r.success) return null + return { username: r.username, role: r.role } + }) + .catch(err => console.error(err)) +} + +const validateResetPasswordLink = token => { + if (!token) return null + return users.validatePasswordResetToken(token) + .then(r => { + if (!r.success) return null + return { id: r.userID } + }) + .catch(err => console.error(err)) +} + +const validateReset2FALink = token => { + if (!token) return null + return users.validate2FAResetToken(token) + .then(r => { + if (!r.success) return null + return users.findById(r.userID) + }) + .then(user => { + const secret = otplib.authenticator.generateSecret() + const otpauth = otplib.authenticator.keyuri(user.username, 'Lamassu Industries', secret) + return { user_id: user.id, secret, otpauth } + }) + .catch(err => console.error(err)) +} + +const deleteSession = (sessionID, context) => { + if (sessionID === context.req.session.id) { + context.req.session.destroy() + } + return sessionManager.deleteSession(sessionID) +} + +const login = (username, password) => { + return authenticateUser(username, password).then(user => { + if (!user) return 'FAILED' + return users.get2FASecret(user.id).then(user => { + const twoFASecret = user.twofa_code + return twoFASecret ? 'INPUT2FA' : 'SETUP2FA' + }) + }) +} + +const input2FA = (username, password, rememberMe, code, context) => { + return authenticateUser(username, password).then(user => { + if (!user) return false + + return users.get2FASecret(user.id).then(user => { + const secret = user.twofa_code + const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret }) + if (!isCodeValid) return false + + const finalUser = { id: user.id, username: user.username, role: user.role } + context.req.session.user = finalUser + if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE + + return true + }) + }) +} + +const setup2FA = (username, password, secret, codeConfirmation) => { + return authenticateUser(username, password).then(user => { + if (!user || !secret) return false + + const isCodeValid = otplib.authenticator.verify({ token: codeConfirmation, secret: secret }) + if (!isCodeValid) return false + + users.save2FASecret(user.id, secret) + return true + }) +} + +const createResetPasswordToken = userID => { + return users.findById(userID) + .then(user => { + if (!user) return null + return users.createResetPasswordToken(user.id) + }) + .then(token => { + return token + }) + .catch(err => console.error(err)) +} + +const createReset2FAToken = userID => { + return users.findById(userID) + .then(user => { + if (!user) return null + return users.createReset2FAToken(user.id) + }) + .then(token => { + return token + }) + .catch(err => console.error(err)) +} + +const createRegisterToken = (username, role) => { + return users.getByName(username) + .then(user => { + if (user) return null + + return users.createUserRegistrationToken(username, role).then(token => { + return token + }) + }) + .catch(err => console.error(err)) +} + +const register = (username, password, role) => { + return users.getByName(username) + .then(user => { + if (user) return false + + users.createUser(username, password, role) + return true + }) + .catch(err => console.error(err)) +} + +const resetPassword = (userID, newPassword, context) => { + return users.findById(userID).then(user => { + if (!user) return false + if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy() + return users.updatePassword(user.id, newPassword) + }).then(() => { return true }).catch(err => console.error(err)) +} + +const reset2FA = (userID, code, secret, context) => { + return users.findById(userID).then(user => { + const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret }) + if (!isCodeValid) return false + + if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy() + return users.save2FASecret(user.id, secret).then(() => { return true }) + }).catch(err => console.error(err)) +} + +module.exports = { + getUserData, + get2FASecret, + confirm2FA, + validateRegisterLink, + validateResetPasswordLink, + validateReset2FALink, + deleteSession, + login, + input2FA, + setup2FA, + createResetPasswordToken, + createReset2FAToken, + createRegisterToken, + register, + resetPassword, + reset2FA +} diff --git a/lib/new-admin/graphql/resolvers/index.js b/lib/new-admin/graphql/resolvers/index.js index b54c25b9..60ed22c0 100644 --- a/lib/new-admin/graphql/resolvers/index.js +++ b/lib/new-admin/graphql/resolvers/index.js @@ -16,6 +16,7 @@ const scalar = require('./scalar.resolver') const settings = require('./settings.resolver') const status = require('./status.resolver') const transaction = require('./transaction.resolver') +const user = require('./users.resolver') const version = require('./version.resolver') const resolvers = [ @@ -35,6 +36,7 @@ const resolvers = [ settings, status, transaction, + user, version ] diff --git a/lib/new-admin/graphql/resolvers/users.resolver.js b/lib/new-admin/graphql/resolvers/users.resolver.js new file mode 100644 index 00000000..846cab57 --- /dev/null +++ b/lib/new-admin/graphql/resolvers/users.resolver.js @@ -0,0 +1,35 @@ +const authentication = require('../modules/authentication') +const users = require('../../../users') +const sessionManager = require('../../../session-manager') + +const resolver = { + Query: { + users: () => users.getUsers(), + sessions: () => sessionManager.getSessionList(), + userSessions: (...[, { username }]) => sessionManager.getUserSessions(username), + userData: (root, args, context, info) => authentication.getUserData(context), + get2FASecret: (...[, { username, password }]) => authentication.get2FASecret(username, password), + confirm2FA: (root, args, context, info) => authentication.confirm2FA(args.code, context), + validateRegisterLink: (...[, { token }]) => authentication.validateRegisterLink(token), + validateResetPasswordLink: (...[, { token }]) => authentication.validateResetPasswordLink(token), + validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token) + }, + Mutation: { + deleteUser: (...[, { id }]) => users.deleteUser(id), + deleteSession: (root, args, context, info) => authentication.deleteSession(args.sid, context), + deleteUserSessions: (...[, { username }]) => sessionManager.deleteUserSessions(username), + changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole), + toggleUserEnable: (...[, { id }]) => users.toggleUserEnable(id), + login: (...[, { username, password }]) => authentication.login(username, password), + input2FA: (root, args, context, info) => authentication.input2FA(args.username, args.password, args.rememberMe, args.code, context), + setup2FA: (...[, { username, password, secret, codeConfirmation }]) => authentication.setup2FA(username, password, secret, codeConfirmation), + createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID), + createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID), + createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role), + register: (...[, { username, password, role }]) => authentication.register(username, password, role), + resetPassword: (root, args, context, info) => authentication.resetPassword(args.userID, args.newPassword, context), + reset2FA: (root, args, context, info) => authentication.reset2FA(args.userID, args.code, args.secret, context) + } +} + +module.exports = resolver diff --git a/lib/new-admin/graphql/types/index.js b/lib/new-admin/graphql/types/index.js index 3e4531af..390e5924 100644 --- a/lib/new-admin/graphql/types/index.js +++ b/lib/new-admin/graphql/types/index.js @@ -16,6 +16,7 @@ const scalar = require('./scalar.type') const settings = require('./settings.type') const status = require('./status.type') const transaction = require('./transaction.type') +const user = require('./users.type') const version = require('./version.type') const types = [ @@ -35,6 +36,7 @@ const types = [ settings, status, transaction, + user, version ] diff --git a/lib/new-admin/graphql/types/users.type.js b/lib/new-admin/graphql/types/users.type.js new file mode 100644 index 00000000..ee99b5fa --- /dev/null +++ b/lib/new-admin/graphql/types/users.type.js @@ -0,0 +1,77 @@ +const typeDef = ` + directive @auth( + requires: [Role] = [USER, SUPERUSER] + ) on OBJECT | FIELD_DEFINITION + + enum Role { + SUPERUSER + USER + } + + type UserSession { + sid: String! + sess: JSONObject! + expire: Date! + } + + type User { + id: ID + username: String + role: String + enabled: Boolean + created: Date + last_accessed: Date + last_accessed_from: String + last_accessed_address: String + } + + type TwoFactorSecret { + user_id: ID + secret: String! + otpauth: String! + } + + type ResetToken { + token: String + user_id: ID + expire: Date + } + + type RegistrationToken { + token: String + username: String + role: String + expire: Date + } + + type Query { + users: [User] @auth(requires: [SUPERUSER]) + sessions: [UserSession] @auth(requires: [SUPERUSER]) + userSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER]) + userData: User + get2FASecret(username: String!, password: String!): TwoFactorSecret + confirm2FA(code: String!): Boolean @auth(requires: [SUPERUSER]) + validateRegisterLink(token: String!): User + validateResetPasswordLink(token: String!): User + validateReset2FALink(token: String!): TwoFactorSecret + } + + type Mutation { + deleteUser(id: ID!): User @auth(requires: [SUPERUSER]) + deleteSession(sid: String!): UserSession @auth(requires: [SUPERUSER]) + deleteUserSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER]) + changeUserRole(id: ID!, newRole: String!): User @auth(requires: [SUPERUSER]) + toggleUserEnable(id: ID!): User @auth(requires: [SUPERUSER]) + login(username: String!, password: String!): String + input2FA(username: String!, password: String!, code: String!, rememberMe: Boolean!): Boolean + setup2FA(username: String!, password: String!, secret: String!, codeConfirmation: String!): Boolean + createResetPasswordToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER]) + createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER]) + createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER]) + register(username: String!, password: String!, role: String!): Boolean + resetPassword(userID: ID!, newPassword: String!): Boolean + reset2FA(userID: ID!, secret: String!, code: String!): Boolean + } +` + +module.exports = typeDef diff --git a/lib/new-admin/routes/auth.js b/lib/new-admin/routes/auth.js index 7ea9a870..1df1a983 100644 --- a/lib/new-admin/routes/auth.js +++ b/lib/new-admin/routes/auth.js @@ -1,259 +1,17 @@ -const otplib = require('otplib') -const bcrypt = require('bcrypt') +const express = require('express') +const router = express.Router() -const users = require('../../users') -const login = require('../login') +const getUserData = function (req, res, next) { + const lidCookie = req.cookies && req.cookies.lid + if (!lidCookie) { + res.sendStatus(403) + return + } -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 + const user = req.session.user + return res.status(200).json({ message: 'Success', user: user }) } -module.exports = function (app) { - app.post('/api/login', function (req, res, next) { - const usernameInput = req.body.username - const passwordInput = req.body.password +router.get('/user-data', getUserData) - 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) - }) - }) -} +module.exports = router diff --git a/new-lamassu-admin/src/lamassu/App.js b/new-lamassu-admin/src/lamassu/App.js index e4a6d3b1..3baed658 100644 --- a/new-lamassu-admin/src/lamassu/App.js +++ b/new-lamassu-admin/src/lamassu/App.js @@ -9,7 +9,7 @@ import { import { axios } from '@use-hooks/axios' import { create } from 'jss' import extendJss from 'jss-plugin-extend' -import React, { createContext, useContext, useEffect, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react' import { useLocation, useHistory, diff --git a/new-lamassu-admin/src/pages/Authentication/Input2FAState.js b/new-lamassu-admin/src/pages/Authentication/Input2FAState.js index 3e70d77c..63c4ad27 100644 --- a/new-lamassu-admin/src/pages/Authentication/Input2FAState.js +++ b/new-lamassu-admin/src/pages/Authentication/Input2FAState.js @@ -1,5 +1,6 @@ +import { useMutation, useLazyQuery } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/core/styles' -import axios from 'axios' +import gql from 'graphql-tag' import React, { useContext, useState } from 'react' import { useHistory } from 'react-router-dom' @@ -10,11 +11,34 @@ import { H2, P } from 'src/components/typography' import styles from './Login.styles' -const url = - process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' - const useStyles = makeStyles(styles) +const INPUT_2FA = gql` + mutation input2FA( + $username: String! + $password: String! + $code: String! + $rememberMe: Boolean! + ) { + input2FA( + username: $username + password: $password + code: $code + rememberMe: $rememberMe + ) + } +` + +const GET_USER_DATA = gql` + { + userData { + id + username + role + } + } +` + const Input2FAState = ({ twoFAField, onTwoFAChange, @@ -33,53 +57,25 @@ const Input2FAState = ({ setInvalidToken(false) } - const handle2FA = () => { - axios({ - method: 'POST', - url: `${url}/api/login/2fa`, - data: { - username: clientField, - password: passwordField, - rememberMe: rememberMeField, - twoFACode: twoFAField - }, - withCredentials: true, - headers: { - 'Content-Type': 'application/json' - } - }) - .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 [input2FA, { error: mutationError }] = useMutation(INPUT_2FA, { + onCompleted: ({ input2FA: success }) => { + success ? getUserData() : setInvalidToken(true) + } + }) - const getUserData = () => { - axios({ - method: 'GET', - url: `${url}/user-data`, - withCredentials: true - }) - .then(res => { - if (res.status === 200) setUserData(res.data.user) - }) - .catch(err => { - if (err.status === 403) setUserData(null) - }) + const [getUserData, { error: queryError }] = useLazyQuery(GET_USER_DATA, { + onCompleted: ({ userData }) => { + setUserData(userData) + history.push('/') + } + }) + + const getErrorMsg = () => { + if (mutationError || queryError) return 'Internal server error' + if (twoFAField.length !== 6 && invalidToken) + return 'The code should have 6 characters!' + if (invalidToken) return 'Code is invalid. Please try again.' + return null } return ( @@ -93,16 +89,26 @@ const Input2FAState = ({ onChange={handle2FAChange} numInputs={6} error={invalidToken} + shouldAutoFocus />
- {invalidToken && ( -

- Code is invalid. Please try again. -

+ {getErrorMsg() && ( +

{getErrorMsg()}

)} +
+ )} - - + ) } diff --git a/new-lamassu-admin/src/pages/Authentication/Register.js b/new-lamassu-admin/src/pages/Authentication/Register.js index 2a3391e0..63fb4fdc 100644 --- a/new-lamassu-admin/src/pages/Authentication/Register.js +++ b/new-lamassu-admin/src/pages/Authentication/Register.js @@ -1,103 +1,97 @@ +import { useQuery, useMutation } from '@apollo/react-hooks' import { makeStyles, Grid } from '@material-ui/core' import Paper from '@material-ui/core/Paper' -import axios from 'axios' -import React, { useState, useEffect } from 'react' +import { Field, Form, Formik } from 'formik' +import gql from 'graphql-tag' +import React, { useState } from 'react' import { useLocation, useHistory } from 'react-router-dom' +import * as Yup from 'yup' import { Button } from 'src/components/buttons' -import { TextInput } from 'src/components/inputs/base' +import { SecretInput } from 'src/components/inputs/formik' import { H2, Label2, P } from 'src/components/typography' import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg' import styles from './Login.styles' -const useQuery = () => new URLSearchParams(useLocation().search) +const QueryParams = () => new URLSearchParams(useLocation().search) const useStyles = makeStyles(styles) -const url = - process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' +const VALIDATE_REGISTER_LINK = gql` + query validateRegisterLink($token: String!) { + validateRegisterLink(token: $token) { + username + role + } + } +` + +const REGISTER = gql` + mutation register($username: String!, $password: String!, $role: String!) { + register(username: $username, password: $password, role: $role) + } +` + +const validationSchema = Yup.object().shape({ + password: Yup.string() + .required('A password is required') + .test( + 'len', + 'Your password must contain more than 8 characters', + val => val.length >= 8 + ), + confirmPassword: Yup.string().oneOf( + [Yup.ref('password'), null], + 'Passwords must match' + ) +}) + +const initialValues = { + password: '', + confirmPassword: '' +} const Register = () => { const classes = useStyles() const history = useHistory() - const query = useQuery() - const [passwordField, setPasswordField] = useState('') - const [confirmPasswordField, setConfirmPasswordField] = useState('') - const [invalidPassword, setInvalidPassword] = useState(false) + const token = QueryParams().get('t') const [username, setUsername] = useState(null) const [role, setRole] = useState(null) const [isLoading, setLoading] = useState(true) const [wasSuccessful, setSuccess] = useState(false) - useEffect(() => { - validateQuery() - }, []) - - const validateQuery = () => { - axios({ - url: `${url}/api/register?t=${query.get('t')}`, - method: 'GET', - options: { - withCredentials: true + const { error: queryError } = useQuery(VALIDATE_REGISTER_LINK, { + variables: { token: token }, + onCompleted: ({ validateRegisterLink: info }) => { + setLoading(false) + if (!info) { + setSuccess(false) + } else { + setSuccess(true) + setUsername(info.username) + setRole(info.role) } - }) - .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('/') - }) - } + }, + onError: () => { + setLoading(false) + setSuccess(false) + } + }) - 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 [register, { error: mutationError }] = useMutation(REGISTER, { + onCompleted: ({ register: success }) => { + if (success) history.push('/wizard', { fromAuthRegister: true }) + } + }) - const isValidPassword = () => { - return passwordField === confirmPasswordField - } - - const handlePasswordChange = event => { - setInvalidPassword(false) - setPasswordField(event.target.value) - } - - const handleConfirmPasswordChange = event => { - setInvalidPassword(false) - setConfirmPasswordField(event.target.value) + const getErrorMsg = (formikErrors, formikTouched) => { + if (!formikErrors || !formikTouched) return null + if (queryError || mutationError) return 'Internal server error' + if (formikErrors.password && formikTouched.password) + return formikErrors.password + if (formikErrors.confirmPassword && formikTouched.confirmPassword) + return formikErrors.confirmPassword + return null } return ( @@ -107,7 +101,6 @@ const Register = () => { direction="column" alignItems="center" justify="center" - style={{ minHeight: '100vh' }} className={classes.welcomeBackground}>
@@ -118,49 +111,52 @@ const Register = () => {

Lamassu Admin

{!isLoading && wasSuccessful && ( - <> - - Insert a password - - - - Confirm password - - -
- {invalidPassword && ( -

- Passwords do not match! -

- )} - -
- + { + register({ + variables: { + username: username, + password: values.password, + role: role + } + }) + }}> + {({ errors, touched }) => ( +
+ + +
+ {getErrorMsg(errors, touched) && ( +

+ {getErrorMsg(errors, touched)} +

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

- Code is invalid. Please try again. -

+ {getErrorMsg() && ( +

{getErrorMsg()}

)} -
- + { + resetPassword({ + variables: { + userID: userID, + newPassword: values.confirmPassword + } + }) + }}> + {({ errors, touched }) => ( +
+ + +
+ {getErrorMsg(errors, touched) && ( +

+ {getErrorMsg(errors, touched)} +

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

- Code is invalid. Please try again. -

+ {getErrorMsg() && ( +

{getErrorMsg()}

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

Change {user.username}'s role?

- + + Change {user.username}'s role? + +

You are about to alter {user.username}'s role. This will change this user's permission to access certain resources. - - Do you wish to proceed? +

+

Do you wish to proceed?

diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/CreateUserModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/CreateUserModal.js index 3fb3a4cc..bc97fbf5 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/CreateUserModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/CreateUserModal.js @@ -1,28 +1,48 @@ +import { useMutation } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/core/styles' -import axios from 'axios' +import classnames from 'classnames' +import { Field, Form, Formik } from 'formik' +import gql from 'graphql-tag' import React, { useState } from 'react' +import * as Yup from 'yup' +import ErrorMessage from 'src/components/ErrorMessage' import Modal from 'src/components/Modal' import { Button } from 'src/components/buttons' -import { RadioGroup } from 'src/components/inputs' -import { TextInput } from 'src/components/inputs/base' +import { TextInput, RadioGroup } from 'src/components/inputs/formik' import { H1, H2, H3, Info3, Mono } from 'src/components/typography' import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard' import styles from '../UserManagement.styles' -const url = - process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' - const useStyles = makeStyles(styles) +const CREATE_USER = gql` + mutation createRegisterToken($username: String!, $role: String!) { + createRegisterToken(username: $username, role: $role) { + token + expire + } + } +` + +const validationSchema = Yup.object().shape({ + username: Yup.string() + .email('Username field should be in an email format!') + .required('Username field is required!'), + role: Yup.string().required('Role field is required!') +}) + +const initialValues = { + username: '', + role: '' +} + const CreateUserModal = ({ showModal, toggleModal }) => { const classes = useStyles() const [usernameField, setUsernameField] = useState('') - const [roleField, setRoleField] = useState('') const [createUserURL, setCreateUserURL] = useState(null) - const [invalidUser, setInvalidUser] = useState(false) const radioOptions = [ { @@ -35,59 +55,27 @@ const CreateUserModal = ({ showModal, toggleModal }) => { } ] - const handleUsernameChange = event => { - if (event.target.value === '') { - setInvalidUser(false) - } - setUsernameField(event.target.value) - } - - const handleRoleChange = event => { - setRoleField(event.target.value) - } - const handleClose = () => { - setUsernameField('') - setRoleField('') - setInvalidUser(false) setCreateUserURL(null) toggleModal() } - const handleCreateUser = () => { - const username = usernameField.trim() - - if (username === '') { - setInvalidUser(true) - return + const [createUser, { error }] = useMutation(CREATE_USER, { + onCompleted: ({ createRegisterToken: token }) => { + setCreateUserURL(`https://localhost:3001/register?t=${token.token}`) } - 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') - }) + }) + + const roleClass = (formikErrors, formikTouched) => ({ + [classes.error]: formikErrors.role && formikTouched.role + }) + + const getErrorMsg = (formikErrors, formikTouched) => { + if (!formikErrors || !formikTouched) return null + if (error) return 'Internal server error' + if (formikErrors.username && formikTouched.username) + return formikErrors.username + return null } return ( @@ -99,38 +87,60 @@ const CreateUserModal = ({ showModal, toggleModal }) => { height={400} handleClose={handleClose} open={true}> -

Create new user

-

User login

- -

Role

- -
- -
+ { + setUsernameField(values.username) + createUser({ + variables: { username: values.username, role: values.role } + }) + }}> + {({ errors, touched }) => ( +
+

Create new user

+ +

+ Role +

+ +
+ {getErrorMsg(errors, touched) && ( + {getErrorMsg(errors, touched)} + )} + +
+ + )} +
)} {showModal && createUserURL && (

Creating {usernameField}...

diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/DeleteUserModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/DeleteUserModal.js index b8940581..ff1113bd 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/DeleteUserModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/DeleteUserModal.js @@ -3,7 +3,7 @@ import React from 'react' import Modal from 'src/components/Modal' import { Button } from 'src/components/buttons' -import { H2, Info3 } from 'src/components/typography' +import { Info2, P } from 'src/components/typography' import styles from '../UserManagement.styles' @@ -32,16 +32,17 @@ const DeleteUserModal = ({ height={275} handleClose={handleClose} open={true}> -

Delete {user.username}?

- + Delete {user.username}? +

You are about to delete {user.username}. This will remove existent sessions and revoke this user's permissions to access the system. - - +

+

This is a PERMANENT operation. Do you wish to proceed? - +

diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js index 1f26d020..bb63b2c7 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js @@ -3,7 +3,7 @@ import React from 'react' import Modal from 'src/components/Modal' import { Button } from 'src/components/buttons' -import { H2, Info3 } from 'src/components/typography' +import { Info2, P } from 'src/components/typography' import styles from '../UserManagement.styles' @@ -28,34 +28,39 @@ const EnableUserModal = ({ {showModal && ( {!user.enabled && ( <> -

Enable {user.username}?

- + + Enable {user.username}? + +

You are about to enable {user.username} into the system, activating previous eligible sessions and grant permissions to access the system. - - Do you wish to proceed? +

+

Do you wish to proceed?

)} {user.enabled && ( <> -

Disable {user.username}?

- + + Disable {user.username}? + +

You are about to disable {user.username} from the system, deactivating previous eligible sessions and removing permissions to access the system. - - Do you wish to proceed? +

+

Do you wish to proceed?

)}
diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js index 07160924..2e96b335 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js @@ -1,19 +1,23 @@ +import { useLazyQuery } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/core/styles' -import axios from 'axios' +import gql from 'graphql-tag' import React, { useState } from 'react' import Modal from 'src/components/Modal' import { Button } from 'src/components/buttons' import { CodeInput } from 'src/components/inputs/base' -import { H2, Info3, P } from 'src/components/typography' +import { Info2, P } from 'src/components/typography' import styles from '../UserManagement.styles' -const url = - process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' - const useStyles = makeStyles(styles) +const CONFIRM_2FA = gql` + query confirm2FA($code: String!) { + confirm2FA(code: $code) + } +` + const Input2FAModal = ({ showModal, toggleModal, action, vars }) => { const classes = useStyles() @@ -31,32 +35,23 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => { toggleModal() } - const handleActionConfirm = () => { - axios({ - method: 'POST', - url: `${url}/api/confirm2fa`, - data: { - code: twoFACode - }, - withCredentials: true, - headers: { - 'Content-Type': 'application/json' + const [confirm2FA, { error: queryError }] = useLazyQuery(CONFIRM_2FA, { + onCompleted: ({ confirm2FA: success }) => { + if (!success) { + setInvalidCode(true) + } else { + action() + handleClose() } - }) - .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) - }) + } + }) + + const getErrorMsg = () => { + if (queryError) return 'Internal server error' + if (twoFACode.length !== 6 && invalidCode) + return 'The code should have 6 characters!' + if (invalidCode) return 'Code is invalid. Please try again.' + return null } return ( @@ -64,15 +59,15 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => { {showModal && ( -

Confirm action

- - Please confirm this action by placing your two-factor authentication - code below. - + Confirm action +

+ To make changes on this user, please confirm this action by entering + your two-factor authentication code below. +

{ containerStyle={classes.codeContainer} shouldAutoFocus /> - {invalidCode && ( -

- Code is invalid. Please try again. -

+ {getErrorMsg() && ( +

{getErrorMsg()}

)}
- +
)} diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js index bffaff91..34ced07c 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js @@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles' import React from 'react' import Modal from 'src/components/Modal' -import { H2, Info3, Mono } from 'src/components/typography' +import { Info2, P, Mono } from 'src/components/typography' import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard' import styles from '../UserManagement.styles' @@ -21,19 +21,24 @@ const Reset2FAModal = ({ showModal, toggleModal, reset2FAURL, user }) => { {showModal && ( -

Reset 2FA for {user.username}

- + + Reset 2FA for {user.username} + +

Safely share this link with {user.username} for a two-factor authentication reset. - +

- + {reset2FAURL} diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js index 0dbe92ea..818d7eaf 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js @@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles' import React from 'react' import Modal from 'src/components/Modal' -import { H2, Info3, Mono } from 'src/components/typography' +import { Info2, P, Mono } from 'src/components/typography' import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard' import styles from '../UserManagement.styles' @@ -26,20 +26,23 @@ const ResetPasswordModal = ({ {showModal && ( -

+ Reset password for {user.username} -

- + +

Safely share this link with {user.username} for a password reset. - +

- + {resetPasswordURL} diff --git a/new-lamassu-admin/src/pazuz/App.js b/new-lamassu-admin/src/pazuz/App.js index e8eb77f3..3baed658 100644 --- a/new-lamassu-admin/src/pazuz/App.js +++ b/new-lamassu-admin/src/pazuz/App.js @@ -6,9 +6,10 @@ import { MuiThemeProvider, makeStyles } from '@material-ui/core/styles' +import { axios } from '@use-hooks/axios' import { create } from 'jss' import extendJss from 'jss-plugin-extend' -import React, { useContext, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react' import { useLocation, useHistory, @@ -72,7 +73,7 @@ const Main = () => { const classes = useStyles() const location = useLocation() const history = useHistory() - const { wizardTested } = useContext(AppContext) + const { wizardTested, userData } = useContext(AppContext) const route = location.pathname @@ -91,7 +92,9 @@ const Main = () => { return (
- {!is404 && wizardTested &&
} + {!is404 && wizardTested && userData && ( +
+ )}
{sidebar && !is404 && wizardTested && ( @@ -117,19 +120,47 @@ const Main = () => { const App = () => { const [wizardTested, setWizardTested] = useState(false) + const [userData, setUserData] = useState(null) + const [loading, setLoading] = useState(true) + + const url = + process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' + + useEffect(() => { + getUserData() + }, []) + + const getUserData = () => { + axios({ + method: 'GET', + url: `${url}/user-data`, + withCredentials: true + }) + .then(res => { + setLoading(false) + if (res.status === 200) setUserData(res.data.user) + }) + .catch(err => { + setLoading(false) + if (err.status === 403) setUserData(null) + }) + } return ( - - - - - - -
- - - - + + {!loading && ( + + + + + +
+ + + + + )} ) } diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index f56bbd4c..c754fb6c 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -38,6 +38,7 @@ import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' import ServerLogs from 'src/pages/ServerLogs' import Services from 'src/pages/Services/Services' +// import TokenManagement from 'src/pages/TokenManagement/TokenManagement' import SessionManagement from 'src/pages/SessionManagement/SessionManagement' import Transactions from 'src/pages/Transactions/Transactions' import Triggers from 'src/pages/Triggers' @@ -247,6 +248,7 @@ const tree = [ key: 'promo-codes', label: 'Promo Codes', route: '/compliance/loyalty/codes', + allowedRoles: [ROLES.USER, ROLES.SUPERUSER], component: PromoCodes }, { @@ -397,12 +399,12 @@ const Routes = () => { + {/* */} - {/* */} {getFilteredRoutes().map(({ route, component: Page, key }) => ( - + {
} /> - + ))} diff --git a/new-lamassu-admin/src/utils/apollo.js b/new-lamassu-admin/src/utils/apollo.js index 564ad78c..0a2b58f3 100644 --- a/new-lamassu-admin/src/utils/apollo.js +++ b/new-lamassu-admin/src/utils/apollo.js @@ -7,7 +7,7 @@ import { HttpLink } from 'apollo-link-http' import React, { useContext } from 'react' import { useHistory, useLocation } from 'react-router-dom' -import { AppContext } from 'src/App' +import AppContext from 'src/AppContext' const URI = process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' diff --git a/package-lock.json b/package-lock.json index a41414b4..b97bdc20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,14 @@ "zen-observable": "^0.8.14" }, "dependencies": { + "@wry/equality": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.1.tgz", + "integrity": "sha512-8/Ftr3jUZ4EXhACfSwPIfNsE8V6WKesdjp+Dxi78Bej6qlasAxiz0/F8j0miACRj9CL4vC5Y5FsfwwEYAuhWbg==", + "requires": { + "tslib": "^1.14.1" + } + }, "symbol-observable": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", @@ -512,6 +520,13 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } } }, "@babel/plugin-syntax-nullish-coalescing-operator": { @@ -538,6 +553,13 @@ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "requires": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } } }, "@babel/plugin-syntax-optional-catch-binding": { @@ -757,6 +779,11 @@ "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" } } }, @@ -768,6 +795,18 @@ "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.12.7", "@babel/types": "^7.12.7" + }, + "dependencies": { + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } } }, "@babel/traverse": { @@ -2531,14 +2570,6 @@ "tslib": "^1.14.1" } }, - "@wry/equality": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.1.tgz", - "integrity": "sha512-8/Ftr3jUZ4EXhACfSwPIfNsE8V6WKesdjp+Dxi78Bej6qlasAxiz0/F8j0miACRj9CL4vC5Y5FsfwwEYAuhWbg==", - "requires": { - "tslib": "^1.14.1" - } - }, "@wry/trie": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.2.1.tgz",