From b38a81e7ac8b0fe06dd96cdbe9c65f217491768d Mon Sep 17 00:00:00 2001 From: Chris Brame Date: Thu, 30 Jun 2022 21:32:53 -0400 Subject: [PATCH] refactor(profile): new profile design and addition fields [unstable] --- src/client/actions/accounts.js | 9 +- src/client/actions/types.js | 1 + src/client/api/index.js | 9 +- src/client/components/Input/index.jsx | 11 +- src/client/components/QRCode/index.jsx | 7 +- .../containers/Modals/PasswordPromptModal.jsx | 79 +++++ src/client/containers/Modals/index.jsx | 4 +- src/client/containers/Profile/index.jsx | 283 +++++++++++++++--- src/client/reducers/shared/index.js | 18 +- src/client/sagas/accounts/index.js | 19 +- src/controllers/api/v2/accounts.js | 115 ++++++- src/controllers/api/v2/routes.js | 9 +- src/controllers/main.js | 1 + src/helpers/viewdata/index.js | 2 + src/models/user.js | 46 +-- src/passport/index.js | 15 + 16 files changed, 556 insertions(+), 72 deletions(-) create mode 100644 src/client/containers/Modals/PasswordPromptModal.jsx diff --git a/src/client/actions/accounts.js b/src/client/actions/accounts.js index deb0e512a..2fc1bd664 100644 --- a/src/client/actions/accounts.js +++ b/src/client/actions/accounts.js @@ -21,7 +21,8 @@ import { FETCH_ACCOUNTS_CREATE_TICKET, SAVE_EDIT_ACCOUNT, UNLOAD_ACCOUNTS, - SAVE_PROFILE + SAVE_PROFILE, + GEN_MFA } from 'actions/types' export const fetchAccounts = createAction( @@ -48,3 +49,9 @@ export const saveProfile = createAction( payload => payload, () => ({ thunk: true }) ) + +export const genMFA = createAction( + GEN_MFA.ACTION, + payload => payload, + () => ({ thunk: true }) +) diff --git a/src/client/actions/types.js b/src/client/actions/types.js index 0909bd01d..db9523648 100644 --- a/src/client/actions/types.js +++ b/src/client/actions/types.js @@ -62,6 +62,7 @@ export const DELETE_ACCOUNT = defineAction('DELETE_ACCOUNT', [PENDING, SUCCESS, export const ENABLE_ACCOUNT = defineAction('ENABLE_ACCOUNT', [SUCCESS, ERROR]) export const UNLOAD_ACCOUNTS = defineAction('UNLOAD_ACCOUNTS', [SUCCESS]) export const SAVE_PROFILE = defineAction('SAVE_PROFILE', [SUCCESS, PENDING, ERROR]) +export const GEN_MFA = defineAction('GEN_MFA', [SUCCESS, PENDING, ERROR]) // Groups export const FETCH_GROUPS = defineAction('FETCH_GROUPS', [PENDING, SUCCESS, ERROR]) diff --git a/src/client/api/index.js b/src/client/api/index.js index da5e89726..453c17568 100644 --- a/src/client/api/index.js +++ b/src/client/api/index.js @@ -15,8 +15,10 @@ import axios from 'axios' axios.defaults.headers.post['Content-Type'] = 'application/json' +const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content') +axios.defaults.headers['CSRF-TOKEN'] = token -let api = {} +const api = {} api.tickets = {} api.tickets.getWithPage = payload => { @@ -183,6 +185,11 @@ api.accounts.saveProfile = payload => { return res.data }) } +api.accounts.generateMFA = payload => { + return axios.post(`/api/v2/accounts/profile/mfa`, payload).then(res => { + return res.data + }) +} api.groups = {} api.groups.create = payload => { diff --git a/src/client/components/Input/index.jsx b/src/client/components/Input/index.jsx index d33a547a2..7e53187ff 100644 --- a/src/client/components/Input/index.jsx +++ b/src/client/components/Input/index.jsx @@ -23,16 +23,23 @@ class Input extends React.Component { } render () { - const { type, defaultValue } = this.props + const { name, type, defaultValue } = this.props return (
- this.handleChange(e)} /> + this.handleChange(e)} + />
) } } Input.propTypes = { + name: PropTypes.string, type: PropTypes.string, defaultValue: PropTypes.string, onChange: PropTypes.func diff --git a/src/client/components/QRCode/index.jsx b/src/client/components/QRCode/index.jsx index f981e6f23..480d193c9 100644 --- a/src/client/components/QRCode/index.jsx +++ b/src/client/components/QRCode/index.jsx @@ -19,8 +19,10 @@ class QRCode extends React.Component { } render () { + let css = {} + if (this.props.css) css = this.props.css return ( -
+
) @@ -29,7 +31,8 @@ class QRCode extends React.Component { QRCode.propTypes = { code: PropTypes.string.isRequired, - size: PropTypes.number + size: PropTypes.number, + css: PropTypes.object } QRCode.defaultProps = { diff --git a/src/client/containers/Modals/PasswordPromptModal.jsx b/src/client/containers/Modals/PasswordPromptModal.jsx new file mode 100644 index 000000000..b706e6038 --- /dev/null +++ b/src/client/containers/Modals/PasswordPromptModal.jsx @@ -0,0 +1,79 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { observer } from 'mobx-react' +import { observable } from 'mobx' + +import { hideModal } from 'actions/common' + +import Input from 'components/Input' +import Button from 'components/Button' +import BaseModal from 'containers/Modals/BaseModal' + +import axios from 'axios' +import helpers from 'lib/helpers' + +@observer +class PasswordPromptModal extends React.Component { + @observable confirmPassword = '' + + onVerifyPassword = e => { + e.preventDefault() + + axios + .post('/api/v2/accounts/profile/mfa/disable', { + confirmPassword: this.confirmPassword + }) + .then(res => { + this.props.hideModal() + + if (this.props.onVerifyComplete) this.props.onVerifyComplete(true) + }) + .catch(error => { + let errMessage = 'An Error has occurred.' + if (error.response && error.response.data && error.response.data.error) errMessage = error.response.data.error + + helpers.UI.showSnackbar(errMessage, true) + + if (this.props.onVerifyComplete) this.props.onVerifyComplete(false) + }) + } + + render () { + const { titleOverride, textOverride } = this.props + return ( + +
+

{titleOverride || 'Confirm Password'}

+

{textOverride || 'Please confirm your password.'}

+
+
+ + (this.confirmPassword = val)} /> +
+
+
+
+ ) + } +} + +PasswordPromptModal.propTypes = { + user: PropTypes.object.isRequired, + titleOverride: PropTypes.string, + textOverride: PropTypes.string, + onVerifyComplete: PropTypes.func.isRequired, + hideModal: PropTypes.func.isRequired +} + +const mapStateToProps = state => ({}) + +export default connect(mapStateToProps, { hideModal })(PasswordPromptModal) diff --git a/src/client/containers/Modals/index.jsx b/src/client/containers/Modals/index.jsx index 42192a2ba..5224bc34f 100644 --- a/src/client/containers/Modals/index.jsx +++ b/src/client/containers/Modals/index.jsx @@ -41,6 +41,7 @@ import EditDepartmentModal from './EditDepartmentModal' import CreateNoticeModal from 'containers/Modals/CreateNoticeModal' import EditNoticeModal from 'containers/Modals/EditNoticeModal' import LinkWarningModal from 'containers/Modals/LinkWarningModal' +import PasswordPromptModal from 'containers/Modals/PasswordPromptModal' const MODAL_COMPONENTS = { NOTICE_ALERT: NoticeAlertModal, @@ -66,7 +67,8 @@ const MODAL_COMPONENTS = { EDIT_DEPARTMENT: EditDepartmentModal, CREATE_NOTICE: CreateNoticeModal, EDIT_NOTICE: EditNoticeModal, - LINK_WARNING: LinkWarningModal + LINK_WARNING: LinkWarningModal, + PASSWORD_PROMPT: PasswordPromptModal } const ModalRoot = ({ modalType, modalProps }) => { diff --git a/src/client/containers/Profile/index.jsx b/src/client/containers/Profile/index.jsx index 242b0f6e2..10b3b33ed 100644 --- a/src/client/containers/Profile/index.jsx +++ b/src/client/containers/Profile/index.jsx @@ -3,9 +3,10 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import { observer } from 'mobx-react' import { makeObservable, observable } from 'mobx' +import axios from 'axios' -import { saveProfile } from 'actions/accounts' -import { setSessionUser } from 'actions/common' +import { saveProfile, genMFA } from 'actions/accounts' +import { showModal, hideModal, setSessionUser } from 'actions/common' import PageTitle from 'components/PageTitle' import PageContent from 'components/PageContent' @@ -21,6 +22,8 @@ import Input from 'components/Input' import QRCode from 'components/QRCode' import TruAccordion from 'components/TruAccordion' +import helpers from 'lib/helpers' + @observer class ProfileContainer extends React.Component { @observable editingProfile = false @@ -39,6 +42,12 @@ class ProfileContainer extends React.Component { @observable currentPassword = null @observable newPassword = null @observable confirmPassword = null + // -- Two Factor + @observable l2Key = null + @observable l2URI = null + @observable l2Step2 = null + @observable l2ShowCantSeeQR = null + @observable l2VerifyText = null constructor (props) { super(props) @@ -84,6 +93,77 @@ class ProfileContainer extends React.Component { onUpdatePasswordClicked = e => { e.preventDefault() + + axios + .post('/api/v2/accounts/profile/update-password', { + currentPassword: this.currentPassword, + newPassword: this.newPassword, + confirmPassword: this.confirmPassword + }) + .then(res => { + if (res.data && res.data.success) { + helpers.UI.showSnackbar('Password Updated Successfully') + setTimeout(() => { + window.location.reload() + }, 1000) + } + }) + .catch(error => { + let errorMsg = 'Invalid Request' + if (error && error.response && error.response.data && error.response.data.error) + errorMsg = error.response.data.error + + helpers.UI.showSnackbar(errorMsg, true) + }) + } + + onEnableMFAClicked = e => { + e.preventDefault() + this.props + .genMFA({ + _id: this.props.sessionUser._id, + username: this.props.sessionUser.username + }) + .then(res => { + this.l2Key = res.key + this.l2URI = res.uri + this.l2Step2 = true + }) + } + + onVerifyMFAClicked = e => { + e.preventDefault() + axios + .post('/api/v2/accounts/profile/mfa/verify', { + tOTPKey: this.l2Key, + code: this.l2VerifyText + }) + .then(res => { + if (res.data && res.data.success) { + // Refresh Session User + this.props.setSessionUser() + this.l2Step2 = null + this.l2ShowCantSeeQR = null + } + }) + .catch(e => { + if (e.response && e.response.data && e.response.data.error) { + helpers.UI.showSnackbar(e.response.data.error, true) + } + }) + } + + onDisableMFAClicked = e => { + e.preventDefault() + const onVerifyComplete = success => { + if (success) { + this.l2Step2 = null + this.l2ShowCantSeeQR = null + this.props.setSessionUser() + } + } + + this.props.showModal('PASSWORD_PROMPT', { user: this.props.sessionUser, onVerifyComplete }) } render () { @@ -119,11 +199,6 @@ class ProfileContainer extends React.Component { return ( <> - {/**/} } @@ -189,10 +264,10 @@ class ProfileContainer extends React.Component {
- - + + - +

Work Information

@@ -273,46 +348,177 @@ class ProfileContainer extends React.Component { )}
- +
-
- - info - -

- After changing your password, you will be logged out of all sessions. -

-
-
-
- - (this.currentPassword = v)} /> +
this.onUpdatePasswordClicked(e)}> +
+ + info + +

+ After changing your password, you will be logged out of all sessions. +

-
- - (this.newPassword = v)} /> +
+
+ + (this.currentPassword = v)} /> +
+
+ + (this.newPassword = v)} /> +
+
+ + (this.confirmPassword = v)} /> +
-
- - (this.confirmPassword = v)} /> +
+
-
+
} /> -

Two-factor authentication is not enabled yet

+ {!this.props.sessionUser.hasL2Auth && ( +
+ {!this.l2Step2 && ( +
+

Two-factor authentication is not enabled yet

+

+ Enabling two-factor authentication adds an extra layer of security to your + accounts. Once enabled, you will be required to enter both your password and an + authentication code in order to sign into your account. After you successfully + enable two-factor authentication, you will not be able to login unless you enter + the correct authentication code. +

+
+
+
+ )} + {this.l2Step2 && ( +
+
+
+

+ Scan the QR code below using any authenticator app such as Authy, Google + Authenticator, LastPass Authenticator, Microsoft Authenticator +

+ + {this.l2ShowCantSeeQR && ( +
+

+ If you are unable to scan the QR code, open the authenticator app and + select the option that allows you to enter the below key manually. +

+

+ + {this.l2Key} + +

+
+ )} +

+ After scanning the QR code, enter the 6-digit verification code below to + activate two-factor authentication on your account. +

+ + (this.l2VerifyText = val)} /> +
+
+
+
+
+ )} +
+ )} + {this.props.sessionUser.hasL2Auth && ( +
+

+ Two-factor authentication is{' '} + + enabled + +

+

+ By disabling two-factor authentication, your account will be protected with only your + password. +

+
+
+
+ )}
} /> @@ -332,7 +538,10 @@ ProfileContainer.propTypes = { sessionUser: PropTypes.object, setSessionUser: PropTypes.func.isRequired, socket: PropTypes.object.isRequired, - saveProfile: PropTypes.func.isRequired + showModal: PropTypes.func.isRequired, + hideModal: PropTypes.func.isRequired, + saveProfile: PropTypes.func.isRequired, + genMFA: PropTypes.func.isRequired } const mapStateToProps = state => ({ @@ -340,4 +549,4 @@ const mapStateToProps = state => ({ socket: state.shared.socket }) -export default connect(mapStateToProps, { saveProfile, setSessionUser })(ProfileContainer) +export default connect(mapStateToProps, { showModal, hideModal, saveProfile, setSessionUser, genMFA })(ProfileContainer) diff --git a/src/client/reducers/shared/index.js b/src/client/reducers/shared/index.js index 5c9c4276e..3f713dc9a 100644 --- a/src/client/reducers/shared/index.js +++ b/src/client/reducers/shared/index.js @@ -15,10 +15,19 @@ import { handleActions } from 'redux-actions' import { fromJS, List, Map } from 'immutable' -import { SET_SESSION_USER, FETCH_ROLES, UPDATE_ROLE_ORDER, SHOW_NOTICE, CLEAR_NOTICE, INIT_SOCKET } from 'actions/types' +import { + SET_SESSION_USER, + FETCH_ROLES, + UPDATE_ROLE_ORDER, + SHOW_NOTICE, + CLEAR_NOTICE, + INIT_SOCKET, + GEN_MFA +} from 'actions/types' const initialState = { sessionUser: null, + mfa: Map({}), roles: List([]), roleOrder: Map({}), notice: null, @@ -51,6 +60,13 @@ const sharedReducer = handleActions( } }, + [GEN_MFA.SUCCESS]: (state, action) => { + return { + ...state, + mfa: fromJS(action.payload) + } + }, + [SHOW_NOTICE]: (state, action) => { return { ...state, diff --git a/src/client/sagas/accounts/index.js b/src/client/sagas/accounts/index.js index 665b20448..19cac822b 100644 --- a/src/client/sagas/accounts/index.js +++ b/src/client/sagas/accounts/index.js @@ -22,7 +22,8 @@ import { HIDE_MODAL, SAVE_EDIT_ACCOUNT, UNLOAD_ACCOUNTS, - SAVE_PROFILE + SAVE_PROFILE, + GEN_MFA } from 'actions/types' import Log from '../../logger' @@ -139,6 +140,21 @@ function * saveProfile ({ payload, meta }) { } } +function * genMFA ({ payload, meta }) { + try { + const response = yield call(api.accounts.generateMFA, payload) + yield put({ type: GEN_MFA.SUCCESS, payload: response, meta }) + } catch (error) { + const errorText = error.response ? error.response.data.error : error + if (error.response && error.response.status !== (401 || 403)) { + Log.error(errorText, error) + helpers.UI.showSnackbar(`Error: ${errorText}`, true) + } + + yield put({ type: GEN_MFA.ERROR, error }) + } +} + export default function * watcher () { yield takeLatest(CREATE_ACCOUNT.ACTION, createAccount) yield takeLatest(FETCH_ACCOUNTS.ACTION, fetchAccounts) @@ -148,4 +164,5 @@ export default function * watcher () { yield takeEvery(ENABLE_ACCOUNT.ACTION, enableAccount) yield takeLatest(UNLOAD_ACCOUNTS.ACTION, unloadThunk) yield takeLatest(SAVE_PROFILE.ACTION, saveProfile) + yield takeLatest(GEN_MFA.ACTION, genMFA) } diff --git a/src/controllers/api/v2/accounts.js b/src/controllers/api/v2/accounts.js index 5a933b41a..e3d31797b 100644 --- a/src/controllers/api/v2/accounts.js +++ b/src/controllers/api/v2/accounts.js @@ -22,6 +22,8 @@ const Group = require('../../../models/group') const Team = require('../../../models/team') const Department = require('../../../models/department') const passwordComplexity = require('../../../settings/passwordComplexity') +const SettingsUtil = require('../../../settings/settingsUtil') +const Session = require('../../../models/session') const accountsApi = {} @@ -44,7 +46,6 @@ accountsApi.sessionUser = async (req, res) => { delete clonedUser.__v delete clonedUser.iOSDeviceTokens delete clonedUser.deleted - delete clonedUser.hasL2Auth clonedUser.groups = groups return res.json(clonedUser) @@ -78,7 +79,7 @@ accountsApi.create = async function (req, res) { let groups = [] if (postData.groups) { groups = await Group.getGroups(postData.groups) - for (const group in groups) { + for (const group of groups) { await group.addMember(savedId) await group.save() } @@ -362,7 +363,6 @@ accountsApi.saveProfile = async (req, res) => { const payload = req.body const user = req.user - console.log(payload) if (payload.username !== user.username || payload._id.toString() !== user._id.toString()) return apiUtil.sendApiError(res, 400, 'Invalid User Account') @@ -383,4 +383,113 @@ accountsApi.saveProfile = async (req, res) => { } } +accountsApi.generateMFA = async (req, res) => { + const payload = req.body + const user = req.user + + if (payload.username !== user.username || payload._id.toString() !== user._id.toString()) + return apiUtil.sendApiError(res, 400, 'Invalid User Account') + + try { + const dbUser = await User.findOne({ _id: payload._id }) + if (!dbUser) return apiUtil.sendApiError(res, 404, 'Invalid User Account') + + if (!dbUser.hasL2Auth) { + const key = await dbUser.generateL2Auth() + const uri = `otpauth://totp/Trudesk:${dbUser.username}-${req.hostname}?secret=${key}&issuer=Trudesk` + + return apiUtil.sendApiSuccess(res, { key, uri }) + } else { + return apiUtil.sendApiError(res, 400, 'Invalid Account') + } + } catch (e) { + return apiUtil.sendApiError(res, 500, e) + } +} + +accountsApi.verifyMFA = async (req, res) => { + const payload = req.body + if (!payload.tOTPKey) return apiUtil.sendApiError(res, 400, 'Invalid Verification') + + req.user.tOTPKey = payload.tOTPKey + + const passport = require('../../../passport') + passport().authenticate('totp-verify', (err, success) => { + if (err || !success) return apiUtil.sendApiError(res, 400, 'Invalid Verification') + + User.findOne({ _id: req.user._id }, function (err, user) { + if (err) return apiUtil.sendApiError(res, 404, 'Invalid Verification') + + user.tOTPKey = req.user.tOTPKey + user.tOTPPeriod = 30 + user.hasL2Auth = true + + user.save(function (err) { + if (err) return apiUtil.sendApiError(res, 500, err.message) + + return apiUtil.sendApiSuccess(res) + }) + }) + })(req, res) +} + +accountsApi.disableMFA = async (req, res) => { + const payload = req.body + if (!payload.confirmPassword) return apiUtil.sendApiError(res, 400, 'Invalid Credentials') + + try { + let user = await User.findOne({ _id: req.user }, '+password') + if (!user) return apiUtil.sendApiError(res, 400, 'Invalid Account') + + if (!User.validate(payload.confirmPassword, user.password)) + return apiUtil.sendApiError(res, 400, 'Invalid Credentials') + + user.tOTPKey = null + user.tOTPPeriod = null + user.hasL2Auth = false + + user = await user.save() + return apiUtil.sendApiSuccess(res) + } catch (e) { + return apiUtil.sendApiError(res, 500, e.message) + } +} + +accountsApi.updatePassword = async (req, res) => { + const payload = req.body + const user = req.user + if (!payload.currentPassword || !payload.newPassword || !payload.confirmPassword) + return apiUtil.sendApiError(res, 400, 'Invalid Post Data') + + if (payload.newPassword !== payload.confirmPassword) return apiUtil.sendApiError(res, 400, 'Invalid Post Data') + + try { + let dbUser = await User.findOne({ _id: user._id }, '+password') + if (!dbUser) return apiUtil.sendApiError(res, 400, 'Invalid User') + + if (!User.validate(payload.currentPassword, dbUser.password)) + return apiUtil.sendApiError(res, 400, 'Invalid Credentials') + + // SETTINGS + const SettingsUtil = require('../../../settings/settingsUtil') + const settingsContent = await SettingsUtil.getSettings() + const settings = settingsContent.data.settings + const passwordComplexityEnabled = settings.accountsPasswordComplexity.value + + if (passwordComplexityEnabled && !passwordComplexity.validate(payload.newPassword)) + throw new Error('Password does not meet requirements') + + dbUser.password = payload.newPassword + + dbUser = await dbUser.save() + + const Session = require('../../../models/session') + await Session.destroy(dbUser._id) + + return apiUtil.sendApiSuccess(res, {}) + } catch (err) { + return apiUtil.sendApiError(res, 500, err.message) + } +} + module.exports = accountsApi diff --git a/src/controllers/api/v2/routes.js b/src/controllers/api/v2/routes.js index fccdd5c38..226573381 100644 --- a/src/controllers/api/v2/routes.js +++ b/src/controllers/api/v2/routes.js @@ -19,6 +19,7 @@ module.exports = function (middleware, router, controllers) { const isAdmin = middleware.isAdmin const isAgent = middleware.isAgent const isAgentOrAdmin = middleware.isAgentOrAdmin + const csrfCheck = middleware.csrfCheck const canUser = middleware.canUser // Common @@ -30,8 +31,12 @@ module.exports = function (middleware, router, controllers) { // Accounts router.get('/api/v2/accounts', apiv2Auth, canUser('accounts:view'), apiv2.accounts.get) router.post('/api/v2/accounts', apiv2Auth, canUser('accounts:create'), apiv2.accounts.create) - router.put('/api/v2/accounts/profile', apiv2Auth, apiv2.accounts.saveProfile) - router.put('/api/v2/accounts/:username', canUser('accounts:update'), apiv2Auth, apiv2.accounts.update) + router.put('/api/v2/accounts/profile', apiv2Auth, csrfCheck, apiv2.accounts.saveProfile) + router.post('/api/v2/accounts/profile/mfa', apiv2Auth, csrfCheck, apiv2.accounts.generateMFA) + router.post('/api/v2/accounts/profile/mfa/verify', apiv2Auth, csrfCheck, apiv2.accounts.verifyMFA) + router.post('/api/v2/accounts/profile/mfa/disable', apiv2Auth, csrfCheck, apiv2.accounts.disableMFA) + router.post('/api/v2/accounts/profile/update-password', apiv2Auth, csrfCheck, apiv2.accounts.updatePassword) + router.put('/api/v2/accounts/:username', apiv2Auth, canUser('accounts:update'), apiv2.accounts.update) // Ticket Info router.get('/api/v2/tickets/info/types', apiv2Auth, apiv2.tickets.info.types) diff --git a/src/controllers/main.js b/src/controllers/main.js index aad770b17..4b3c71b63 100644 --- a/src/controllers/main.js +++ b/src/controllers/main.js @@ -167,6 +167,7 @@ mainController.l2AuthPost = function (req, res, next) { if (!req.user) { return res.redirect('/') } + passport.authenticate('totp', function (err, success) { if (err) { winston.error(err) diff --git a/src/helpers/viewdata/index.js b/src/helpers/viewdata/index.js index 4f9b5a21d..33516e825 100644 --- a/src/helpers/viewdata/index.js +++ b/src/helpers/viewdata/index.js @@ -676,6 +676,8 @@ viewController.getOverdueSetting = function (request, callback) { } viewController.getShowTourSetting = function (request, callback) { + if (!request.user) return callback('Invalid User') + const settingSchema = require('../../models/setting') settingSchema.getSettingByName('showTour:enable', function (err, data) { if (err) { diff --git a/src/models/user.js b/src/models/user.js index 4c06e8610..6097195ac 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -147,30 +147,34 @@ userSchema.methods.removeAccessToken = function (callback) { } userSchema.methods.generateL2Auth = function (callback) { - var user = this - if (_.isUndefined(user.tOTPKey) || _.isNull(user.tOTPKey)) { - var chance = new Chance() - var base32 = require('thirty-two') + const user = this + return new Promise((resolve, reject) => { + ;(async () => { + if (_.isUndefined(user.tOTPKey) || _.isNull(user.tOTPKey)) { + const chance = new Chance() + const base32 = require('thirty-two') + + const genOTPKey = chance.string({ + length: 7, + pool: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789' + }) - var genOTPKey = chance.string({ - length: 7, - pool: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789' - }) - var base32GenOTPKey = base32 - .encode(genOTPKey) - .toString() - .replace(/=/g, '') + const base32GenOTPKey = base32 + .encode(genOTPKey) + .toString() + .replace(/=/g, '') - user.tOTPKey = base32GenOTPKey - user.hasL2Auth = true - user.save(function (err) { - if (err) return callback(err) + if (typeof callback === 'function') return callback(null, base32GenOTPKey) - return callback(null, base32GenOTPKey) - }) - } else { - return callback() - } + return resolve(base32GenOTPKey) + } else { + const error = new Error('FATAL: Key already assigned!') + if (typeof callback === 'function') return callback(error) + + return reject(error) + } + })() + }) } userSchema.methods.removeL2Auth = function (callback) { diff --git a/src/passport/index.js b/src/passport/index.js index fe5771b5b..0730cc115 100644 --- a/src/passport/index.js +++ b/src/passport/index.js @@ -83,6 +83,21 @@ module.exports = function () { ) ) + passport.use( + 'totp-verify', + new TotpStrategy( + { + window: 2 + }, + function (user, done) { + if (!user.tOTPKey) return done(false) + if (!user.tOTPPeriod) user.tOTPPeriod = 30 + + return done(null, base32.decode(user.tOTPKey).toString(), user.tOTPPeriod) + } + ) + ) + passport.use( 'jwt', new JwtStrategy(