From 13dd6c61fc85fa773b4065f075fceda563129c53 Mon Sep 17 00:00:00 2001 From: Chris Brame Date: Wed, 18 May 2022 00:18:45 -0400 Subject: [PATCH] refactor(accounts): password security enhancements --- package.json | 1 + src/client/components/Nav/Sidebar/index.jsx | 6 + .../containers/Modals/CreateAccountModal.jsx | 6 +- .../containers/Settings/Accounts/index.jsx | 146 +++++++ .../containers/Settings/General/index.jsx | 16 - .../containers/Settings/SettingsContainer.jsx | 9 + src/client/sagas/accounts/index.js | 1 + src/controllers/api/apiUtils.js | 2 +- src/controllers/api/v1/users.js | 162 ++++--- src/controllers/api/v2/accounts.js | 400 +++++++----------- src/controllers/settings.js | 8 + src/helpers/viewdata/index.js | 11 +- src/models/department.js | 25 +- src/models/group.js | 35 +- src/models/team.js | 21 +- .../js/angularjs/controllers/profile.js | 9 +- .../formvalidator/jquery.form-validator.js | 7 + src/routes/index.js | 1 + src/settings/passwordComplexity.js | 50 +++ src/settings/settingsUtil.js | 2 + yarn.lock | 5 + 21 files changed, 579 insertions(+), 344 deletions(-) create mode 100644 src/client/containers/Settings/Accounts/index.jsx create mode 100644 src/settings/passwordComplexity.js diff --git a/package.json b/package.json index cc6e7ed75..213fc7c2c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "body-parser": "1.19.2", "busboy": "1.4.0", "chance": "1.1.8", + "check-password-strength": "2.0.5", "cheerio": "1.0.0-rc.10", "clone": "2.1.2", "clsx": "1.1.1", diff --git a/src/client/components/Nav/Sidebar/index.jsx b/src/client/components/Nav/Sidebar/index.jsx index 3fdd2bc93..d970252c7 100644 --- a/src/client/components/Nav/Sidebar/index.jsx +++ b/src/client/components/Nav/Sidebar/index.jsx @@ -280,6 +280,12 @@ class Sidebar extends React.Component { href='/settings' active={activeSubItem === 'settings-general'} /> + this.onInputChanged(e, 'password')} - data-validation={'length'} - data-validation-length={'min6'} - data-validation-error-msg={'Password must contain at least 6 characters.'} + data-validation={this.props.common.accountsPasswordComplexity ? 'length' : 'none'} + data-validation-length={'min8'} + data-validation-error-msg={'Password must contain at least 8 characters.'} />
diff --git a/src/client/containers/Settings/Accounts/index.jsx b/src/client/containers/Settings/Accounts/index.jsx new file mode 100644 index 000000000..485e24f80 --- /dev/null +++ b/src/client/containers/Settings/Accounts/index.jsx @@ -0,0 +1,146 @@ +/* + * . .o8 oooo + * .o8 "888 `888 + * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo + * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P' + * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888. + * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b. + * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o + * ======================================================================== + * Author: Chris Brame + * Updated: 5/17/22 2:20 PM + * Copyright (c) 2014-2022. All rights reserved. + */ + +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { updateSetting, updateMultipleSettings } from 'actions/settings' + +import Button from 'components/Button' +import SettingItem from 'components/Settings/SettingItem' + +import helpers from 'lib/helpers' +import axios from 'axios' +import Log from '../../../logger' +import EnableSwitch from 'components/Settings/EnableSwitch' +import { observer } from 'mobx-react' +import { makeObservable, observable } from 'mobx' +import UIKit from 'uikit' + +@observer +class AccountsSettingsContainer extends React.Component { + @observable passwordComplexityEnabled = false + @observable allowUserRegistrationEnabled = false + + constructor (props) { + super(props) + + makeObservable(this) + + this.state = { + restarting: false + } + + this.restartServer = this.restartServer.bind(this) + } + + componentDidMount () { + // helpers.UI.inputs() + } + + componentDidUpdate (prevProps) { + // helpers.UI.reRenderInputs() + if (prevProps.settings !== this.props.settings) { + if (this.passwordComplexityEnabled !== this.getSetting('accountsPasswordComplexity')) + this.passwordComplexityEnabled = this.getSetting('accountsPasswordComplexity') + if (this.allowUserRegistrationEnabled !== this.getSetting('allowUserRegistration')) + this.allowUserRegistrationEnabled = this.getSetting('allowUserRegistration') + } + } + + restartServer () { + this.setState({ restarting: true }) + + const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content') + axios + .post( + '/api/v1/admin/restart', + {}, + { + headers: { + 'CSRF-TOKEN': token + } + } + ) + .catch(error => { + helpers.hideLoader() + Log.error(error.responseText) + Log.error('Unable to restart server. Server must run under PM2 and Account must have admin rights.') + helpers.UI.showSnackbar('Unable to restart server. Are you an Administrator?', true) + }) + .then(() => { + this.setState({ restarting: false }) + }) + } + + getSetting (stateName) { + return this.props.settings.getIn(['settings', stateName, 'value']) + ? this.props.settings.getIn(['settings', stateName, 'value']) + : '' + } + + updateSetting (stateName, name, value) { + this.props.updateSetting({ stateName, name, value }) + } + + render () { + const { active } = this.props + return ( +
+ { + this.updateSetting('allowUserRegistration', 'allowUserRegistration:enable', e.target.checked) + }} + /> + } + /> + { + this.updateSetting('accountsPasswordComplexity', 'accountsPasswordComplexity:enable', e.target.checked) + }} + /> + } + /> +
+ ) + } +} + +AccountsSettingsContainer.propTypes = { + active: PropTypes.bool.isRequired, + updateSetting: PropTypes.func.isRequired, + updateMultipleSettings: PropTypes.func.isRequired, + settings: PropTypes.object.isRequired +} + +const mapStateToProps = state => ({ + settings: state.settings.settings +}) + +export default connect(mapStateToProps, { updateSetting, updateMultipleSettings })(AccountsSettingsContainer) diff --git a/src/client/containers/Settings/General/index.jsx b/src/client/containers/Settings/General/index.jsx index b74ea8433..8e753f167 100644 --- a/src/client/containers/Settings/General/index.jsx +++ b/src/client/containers/Settings/General/index.jsx @@ -94,17 +94,6 @@ class GeneralSettings extends React.Component { /> ) - const AllowUserRegistration = ( - { - this.updateSetting('allowUserRegistration', 'allowUserRegistration:enable', e.target.checked) - }} - /> - ) - return (
-
) } diff --git a/src/client/containers/Settings/SettingsContainer.jsx b/src/client/containers/Settings/SettingsContainer.jsx index 6a219f345..3ccc66e88 100644 --- a/src/client/containers/Settings/SettingsContainer.jsx +++ b/src/client/containers/Settings/SettingsContainer.jsx @@ -21,6 +21,7 @@ import { fetchSettings } from 'actions/settings' import Menu from 'components/Settings/Menu' import MenuItem from 'components/Settings/MenuItem' import GeneralSettings from './General' +import AccountsSettings from './Accounts' import AppearanceSettings from './Appearance' import PermissionsSettingsContainer from './Permissions' import TicketsSettings from './Tickets' @@ -89,6 +90,13 @@ class SettingsContainer extends React.Component { this.onMenuItemClick(e, 'general') }} /> + { + this.onMenuItemClick(e, 'accounts') + }} + /> (this.page = i)}>
+ diff --git a/src/client/sagas/accounts/index.js b/src/client/sagas/accounts/index.js index 05da8ed95..63eaaeb1d 100644 --- a/src/client/sagas/accounts/index.js +++ b/src/client/sagas/accounts/index.js @@ -80,6 +80,7 @@ function * saveEditAccount ({ payload }) { } catch (error) { let errorText = '' if (error.response) errorText = error.response.data.error + if (errorText.message) errorText = errorText.message helpers.UI.showSnackbar(`Error: ${errorText}`, true) Log.error(errorText, error.response || error) yield put({ type: SAVE_EDIT_ACCOUNT.ERROR, error }) diff --git a/src/controllers/api/apiUtils.js b/src/controllers/api/apiUtils.js index 244925e7a..6f231ac53 100644 --- a/src/controllers/api/apiUtils.js +++ b/src/controllers/api/apiUtils.js @@ -24,7 +24,7 @@ apiUtils.sendApiSuccess = function (res, object) { } apiUtils.sendApiError = function (res, errorNum, error) { - return res.status(errorNum).json({ success: false, error: error }) + return res.status(errorNum).json({ success: false, error }) } apiUtils.sendApiError_InvalidPostData = function (res) { return apiUtils.sendApiError(res, 400, 'Invalid Post Data') diff --git a/src/controllers/api/v1/users.js b/src/controllers/api/v1/users.js index e6c2683ab..11f2aae64 100644 --- a/src/controllers/api/v1/users.js +++ b/src/controllers/api/v1/users.js @@ -14,12 +14,14 @@ const async = require('async') const _ = require('lodash') -const winston = require('winston') +const winston = require('../../../logger') const permissions = require('../../../permissions') const emitter = require('../../../emitter') const UserSchema = require('../../../models/user') const groupSchema = require('../../../models/group') const notificationSchema = require('../../../models/notification') +const SettingUtil = require('../../../settings/settingsUtil') +const Chance = require('chance') const apiUsers = {} @@ -147,7 +149,7 @@ apiUsers.getWithLimit = function (req, res) { "error": "Invalid Post Data" } */ -apiUsers.create = function (req, res) { +apiUsers.create = async function (req, res) { const response = {} response.success = true @@ -174,65 +176,91 @@ apiUsers.create = function (req, res) { if (postData.aPass !== postData.aPassConfirm) return res.status(400).json({ success: false, error: 'Invalid Password Match' }) - const Chance = require('chance') - const chance = new Chance() + async.series( + [ + function (next) { + SettingUtil.getSettings(function (err, content) { + if (err) return next(err) + const settings = content.data.settings + if (settings.accountsPasswordComplexity.value) { + const passwordComplexity = require('../../../settings/passwordComplexity') + if (!passwordComplexity.validate(postData.aPass)) + return next({ message: 'Password does not meet minimum requirements.' }) - const account = new UserSchema({ - username: postData.aUsername, - password: postData.aPass, - fullname: postData.aFullname, - email: postData.aEmail, - accessToken: chance.hash(), - role: postData.aRole - }) + return next() + } - if (postData.aTitle) { - account.title = postData.aTitle - } + return next() + }) + }, + function (next) { + const chance = new Chance() + + const account = new UserSchema({ + username: postData.aUsername, + password: postData.aPass, + fullname: postData.aFullname, + email: postData.aEmail, + accessToken: chance.hash(), + role: postData.aRole + }) - account.save(function (err, a) { - if (err) { - response.success = false - response.error = err - winston.debug(response) - return res.status(400).json(response) - } + if (postData.aTitle) { + account.title = postData.aTitle + } - a.populate('role', function (err, populatedAccount) { - if (err) return res.status(500).json({ success: false, error: err }) + account.save(function (err, a) { + if (err) return next(err) - response.account = populatedAccount.toObject() - delete response.account.password + a.populate('role', function (err, populatedAccount) { + if (err) return next(err) - const groups = [] + response.account = populatedAccount.toObject() + delete response.account.password - async.each( - postData.aGrps, - function (id, done) { - if (_.isUndefined(id)) return done(null) - groupSchema.getGroupById(id, function (err, grp) { - if (err) return done(err) - if (!grp) return done('Invalid Group (' + id + ') - Group not found. Check Group ID') + const groups = [] - grp.addMember(a._id, function (err, success) { - if (err) return done(err) + async.each( + postData.aGrps, + function (id, done) { + if (_.isUndefined(id)) return done() + groupSchema.getGroupById(id, function (err, grp) { + if (err) return done(err) + if (!grp) return done({ message: `Invalid Group (${id}) - Group not found. Check Group ID.` }) + + grp.addMember(a._id, function (err, success) { + if (err) return done(err) + + grp.save(function (err) { + if (err) return done(err) + groups.push(grp) + done(null, success) + }) + }) + }) + }, + function (e) { + if (e) return next(e) + response.account.groups = groups - grp.save(function (err) { - if (err) return done(err) - groups.push(grp) - done(null, success) - }) - }) + return next() + } + ) }) - }, - function (err) { - if (err) return res.status(400).json({ success: false, error: err }) - response.account.groups = groups - return res.json(response) - } - ) - }) - }) + }) + } + ], + function (e) { + if (e) { + response.success = false + response.error = e + winston.debug(response) + return res.status(400).json(response) + } + + return res.json(response) + } + ) } /** @@ -295,6 +323,20 @@ apiUsers.createPublicAccount = function (req, res) { return next(null, roleDefault) }) }, + function (roleDefault, next) { + SettingSchema.getSetting('accountsPasswordComplexity:enable', function (err, passwordComplexitySetting) { + if (err) return next(err) + if (!passwordComplexitySetting || passwordComplexitySetting.value === true) { + const passwordComplexity = require('../../../settings/passwordComplexity') + if (!passwordComplexity.validate(postData.user.password)) + return next({ message: 'Password does not minimum requirements.' }) + + return next(null, roleDefault) + } + + return next(null, roleDefault) + }) + }, function (roleDefault, next) { const UserSchema = require('../../../models/user') user = new UserSchema({ @@ -397,8 +439,20 @@ apiUsers.update = function (req, res) { obj.groups = [obj.groups] } + let passwordComplexityEnabled = true + async.series( { + settings: function (done) { + var SettingUtil = require('../../../settings/settingsUtil') + SettingUtil.getSettings(function (err, content) { + if (err) return done(err) + var settings = content.data.settings + passwordComplexityEnabled = settings.accountsPasswordComplexity.value + + return done() + }) + }, user: function (done) { UserSchema.getUserByUsername(username, function (err, user) { if (err) return done(err) @@ -413,6 +467,12 @@ apiUsers.update = function (req, res) { !_.isEmpty(obj.passconfirm) ) { if (obj.password === obj.passconfirm) { + if (passwordComplexityEnabled) { + // check Password Complexity + const passwordComplexity = require('../../../settings/passwordComplexity') + if (!passwordComplexity.validate(obj.password)) return done('Password does not meet requirements') + } + user.password = obj.password passwordUpdated = true } diff --git a/src/controllers/api/v2/accounts.js b/src/controllers/api/v2/accounts.js index 05a9dbeca..44ad0f32e 100644 --- a/src/controllers/api/v2/accounts.js +++ b/src/controllers/api/v2/accounts.js @@ -14,111 +14,80 @@ const _ = require('lodash') const async = require('async') +const winston = require('../../../logger') const Chance = require('chance') const apiUtil = require('../apiUtils') const User = require('../../../models/user') const Group = require('../../../models/group') const Team = require('../../../models/team') const Department = require('../../../models/department') +const passwordComplexity = require('../../../settings/passwordComplexity') const accountsApi = {} -accountsApi.create = function (req, res) { +accountsApi.create = async function (req, res) { const postData = req.body if (!postData) return apiUtil.sendApiError_InvalidPostData(res) let savedId = null const chance = new Chance() - async.series( - { - user: function (next) { - User.create( - { - username: postData.username, - email: postData.email, - password: postData.password, - fullname: postData.fullname, - title: postData.title, - role: postData.role, - accessToken: chance.hash() - }, - function (err, user) { - if (err) return apiUtil.sendApiError(res, 500, err.message) - savedId = user._id - - return user.populate('role', next) - } - ) - }, - groups: function (next) { - if (!postData.groups) return next(null, []) - - Group.getGroups(postData.groups, function (err, groups) { - if (err) return next(err) - - async.each( - groups, - function (group, callback) { - group.addMember(savedId, function (err) { - if (err) return callback(err) - group.save(callback) - }) - }, - function (err) { - if (err) return next(err) - - return next(null, groups) - } - ) - }) - }, - teams: function (next) { - if (!postData.teams) return next() - - Team.getTeamsByIds(postData.teams, function (err, teams) { - if (err) return next(err) - - async.each( - teams, - function (team, callback) { - team.addMember(savedId, function () { - team.save(callback) - }) - }, - function (err) { - if (err) return next(err) + try { + let user = await User.create({ + username: postData.username, + email: postData.email, + password: postData.password, + fullname: postData.fullname, + title: postData.title, + role: postData.role, + accessToken: chance.hash() + }) + + savedId = user._id + + const userPopulated = await user.populate('role') + + let groups = [] + if (postData.groups) { + groups = await Group.getGroups(postData.groups) + for (const group in groups) { + await group.addMember(savedId) + await group.save() + } + } - return next(null, teams) - } - ) - }) - }, - departments: function (next) { - Department.getUserDepartments(savedId, next) + let teams = [] + if (postData.teams) { + const dbTeams = await Team.getTeamsByIds(postData.teams) + for (const team of dbTeams) { + await team.addMember(savedId) + await team.save() } - }, - function (err, results) { - if (err) return apiUtil.sendApiError(res, 500, err.message) - const user = results.user.toJSON() - user.groups = results.groups.map(function (g) { - return { _id: g._id, name: g.name } - }) + teams = dbTeams + } - if ((user.role.isAgent || user.role.isAdmin) && results.teams) { - user.teams = results.teams.map(function (t) { - return { _id: t._id, name: t.name } - }) + const departments = await Department.getUserDepartments(savedId) + user = userPopulated.toJSON() + user.groups = groups.map(g => { + return { _id: g._id, name: g.name } + }) - user.departments = results.departments.map(function (d) { - return { _id: d._id, name: d.name } - }) - } + if ((user.role.isAgent || user.role.isAdmin) && teams.length > 0) { + user.teams = teams.map(t => { + return { _id: t._id, name: t.name } + }) - return apiUtil.sendApiSuccess(res, { account: user }) + user.departments = departments.map(d => { + return { _id: d._id, name: d.name } + }) } - ) + + return apiUtil.sendApiSuccess(res, { account: user }) + } catch (e) { + winston.warn(e) + return apiUtil.sendApiError(res, 500, e.message) + } } accountsApi.get = function (req, res) { @@ -241,183 +210,124 @@ accountsApi.get = function (req, res) { } } -accountsApi.update = function (req, res) { - var username = req.params.username - var postData = req.body +accountsApi.update = async function (req, res) { + const username = req.params.username + const postData = req.body if (!username || !postData) return apiUtil.sendApiError_InvalidPostData(res) let passwordUpdated = false - async.series( - { - user: function (next) { - User.getByUsername(username, function (err, user) { - if (err) return next(err) - if (!user) return next({ message: 'Invalid User' }) - - postData._id = user._id - - if ( - !_.isUndefined(postData.password) && - !_.isEmpty(postData.password) && - !_.isUndefined(postData.passwordConfirm) && - !_.isEmpty(postData.passwordConfirm) - ) { - if (postData.password === postData.passwordConfirm) { - user.password = postData.password - passwordUpdated = true - } - } - - if (!_.isUndefined(postData.fullname) && postData.fullname.length > 0) user.fullname = postData.fullname - if (!_.isUndefined(postData.email) && postData.email.length > 0) user.email = postData.email - if (!_.isUndefined(postData.title) && postData.title.length > 0) user.title = postData.title - if (!_.isUndefined(postData.role) && postData.role.length > 0) user.role = postData.role - - user.save(function (err, user) { - if (err) return next(err) - - user.populate('role', function (err, populatedUser) { - if (err) return next(err) - var resUser = apiUtil.stripUserFields(populatedUser) - - return next(null, resUser) - }) - }) - }) - }, - groups: function (next) { - if (!postData.groups) return Group.getAllGroupsOfUser(postData._id, next) - - var userGroups = [] - Group.getAllGroups(function (err, groups) { - if (err) return next(err) - async.each( - groups, - function (grp, callback) { - if (_.includes(postData.groups, grp._id.toString())) { - if (grp.isMember(postData._id)) { - userGroups.push(grp) - return callback() - } - grp.addMember(postData._id, function (err, result) { - if (err) return callback(err) - - if (result) { - grp.save(function (err) { - if (err) return callback(err) - userGroups.push(grp) - return callback() - }) - } else { - return callback() - } - }) - } else { - // Remove Member from group - grp.removeMember(postData._id, function (err, result) { - if (err) return callback(err) - if (result) { - grp.save(function (err) { - if (err) return callback(err) - - return callback() - }) - } else { - return callback() - } - }) - } - }, - function (err) { - if (err) return next(err) + try { + // SETTINGS + const SettingsUtil = require('../../../settings/settingsUtil') + const settingsContent = await SettingsUtil.getSettings() + const settings = settingsContent.data.settings + const passwordComplexityEnabled = settings.accountsPasswordComplexity.value + + // USER + let user = await User.getByUsername(username) + if (!user) throw new Error('Invalid User') + + postData._id = user._id + if ( + !_.isUndefined(postData.password) && + !_.isEmpty(postData.password) && + !_.isUndefined(postData.passwordConfirm) && + !_.isEmpty(postData.passwordConfirm) + ) { + if (postData.password === postData.passwordConfirm) { + if (passwordComplexityEnabled) { + if (!passwordComplexity.validate(postData.password)) throw new Error('Password does not meet requirements') + } + + user.password = postData.password + passwordUpdated = true + } else throw new Error('Password and Confirm Password do not match.') + } - return next(null, userGroups) + if (!_.isUndefined(postData.fullname) && postData.fullname.length > 0) user.fullname = postData.fullname + if (!_.isUndefined(postData.email) && postData.email.length > 0) user.email = postData.email + if (!_.isUndefined(postData.title) && postData.title.length > 0) user.title = postData.title + if (!_.isUndefined(postData.role) && postData.role.length > 0) user.role = postData.role + + user = await user.save() + const populatedUser = await user.populate('role') + const resUser = apiUtil.stripUserFields(populatedUser) + + // GROUPS + let groups = [] + if (!postData.groups) groups = await Group.getAllGroupsOfUser(postData._id) + else { + const allGroups = await Group.getAllGroups() + for (const g of allGroups) { + if (_.includes(postData.groups, g._id.toString())) { + if (g.isMember(postData._id)) { + groups.push(g) + } else { + const result = await g.addMember(postData._id) + if (result) { + await g.save() + groups.push(g) } - ) - }) - }, - teams: function (next) { - if (!postData.teams) return Team.getTeamsOfUser(postData._id, next) - - var userTeams = [] - Team.getTeams(function (err, teams) { - if (err) return next(err) - async.each( - teams, - function (team, callback) { - if (_.includes(postData.teams, team._id.toString())) { - if (team.isMember(postData._id)) { - userTeams.push(team) - return callback() - } - team.addMember(postData._id, function (err, result) { - if (err) return callback(err) - - if (result) { - team.save(function (err) { - if (err) return callback(err) - userTeams.push(team) - return callback() - }) - } else { - return callback() - } - }) - } else { - // Remove Member from group - team.removeMember(postData._id, function (err, result) { - if (err) return callback(err) - if (result) { - team.save(function (err) { - if (err) return callback(err) - - return callback() - }) - } else { - return callback() - } - }) - } - }, - function (err) { - if (err) return next(err) + } + } else { + const result = await g.removeMember(postData._id) + if (result) await g.save() + } + } + } - return next(null, userTeams) + // TEAMS + let teams = [] + if (!postData.teams) { + teams = await Team.getTeamsOfUser(postData._id) + } else { + const allTeams = await Team.getTeams() + for (const t of allTeams) { + if (_.includes(postData.teams, t._id.toString())) { + if (t.isMember(postData._id)) teams.push(t) + else { + const result = await t.addMember(postData._id) + if (result) { + await t.save() + teams.push(t) } - ) - }) - }, - departments: function (next) { - Department.getUserDepartments(postData._id, next) + } + } else { + const result = await t.removeMember(postData._id) + if (result) await t.save() + } } - }, - async function (err, results) { - if (err) return apiUtil.sendApiError(res, 500, err.message) + } - var user = results.user.toJSON() - user.groups = results.groups.map(function (g) { - return { _id: g._id, name: g.name } - }) + // DEPARTMENTS + const departments = await Department.getUserDepartments(postData._id) - if ((user.role.isAgent || user.role.isAdmin) && results.teams) { - user.teams = results.teams.map(function (t) { - return { _id: t._id, name: t.name } - }) + user = resUser.toJSON() + user.groups = groups.map(g => { + return { _id: g._id, name: g.name } + }) - user.departments = results.departments.map(function (d) { - return { _id: d._id, name: d.name } - }) - } + if ((user.role.isAgent || user.role.isAdmin) && teams.length > 0) { + user.teams = teams.map(t => { + return { _id: t._id, name: t.name } + }) - if (passwordUpdated) { - const Session = require('../../../models/session') - await Session.destroy(user._id) - } + user.departments = departments.map(d => { + return { _id: d._id, name: d.name } + }) + } - return apiUtil.sendApiSuccess(res, { user: user }) + if (passwordUpdated) { + const Session = require('../../../models/session') + await Session.destroy(user._id) } - ) + + return apiUtil.sendApiSuccess(res, { user }) + } catch (e) { + const error = { name: e.name, message: e.message } + return apiUtil.sendApiError(res, 400, error) + } } module.exports = accountsApi diff --git a/src/controllers/settings.js b/src/controllers/settings.js index ea7002904..f810527ee 100644 --- a/src/controllers/settings.js +++ b/src/controllers/settings.js @@ -76,6 +76,14 @@ settingsController.general = function (req, res) { renderView(res, content) } +settingsController.accounts = function (req, res) { + if (!checkPerms(req, 'settings:view')) return res.redirect('/') + + const content = initViewContent('accounts', req) + + renderView(res, content) +} + settingsController.appearance = function (req, res) { if (!checkPerms(req, 'settings:view')) return res.redirect('/') diff --git a/src/helpers/viewdata/index.js b/src/helpers/viewdata/index.js index 19e45db0b..e11e0748c 100644 --- a/src/helpers/viewdata/index.js +++ b/src/helpers/viewdata/index.js @@ -17,6 +17,7 @@ const _ = require('lodash') const winston = require('../../logger') const moment = require('moment') const settingSchema = require('../../models/setting') +const settingsUtil = require('../../settings/settingsUtil') const viewController = {} const viewdata = {} @@ -356,7 +357,6 @@ viewController.getData = function (request, cb) { }) }, function (callback) { - const settingsUtil = require('../../settings/settingsUtil') settingsUtil.getSettings(function (err, res) { if (err) return callback(err) @@ -365,6 +365,15 @@ viewController.getData = function (request, cb) { return callback() }) }, + function (callback) { + settingsUtil.getSettings(function (err, res) { + if (err) return callback(err) + + viewdata.accountsPasswordComplexity = res.data.settings.accountsPasswordComplexity.value + + return callback() + }) + }, function (callback) { viewController.getPluginsInfo(request, function (err, data) { if (err) return callback(err) diff --git a/src/models/department.js b/src/models/department.js index 1424208bf..0912fcb68 100644 --- a/src/models/department.js +++ b/src/models/department.js @@ -48,16 +48,23 @@ departmentSchema.statics.getDepartmentsByTeam = function (teamIds, callback) { .exec(callback) } -departmentSchema.statics.getUserDepartments = function (userId, callback) { - var self = this - - Teams.getTeamsOfUser(userId, function (err, teams) { - if (err) return callback(err) +departmentSchema.statics.getUserDepartments = async function (userId, callback) { + const self = this + return new Promise((resolve, reject) => { + ;(async () => { + try { + const teams = await Teams.getTeamsOfUser(userId) + const exec = self.model(COLLECTION).find({ teams: { $in: teams } }) + if (typeof callback === 'function') { + return exec.exec(callback) + } - return self - .model(COLLECTION) - .find({ teams: { $in: teams } }) - .exec(callback) + const departments = await exec.exec() + return resolve(departments) + } catch (e) { + return reject(e) + } + })() }) } diff --git a/src/models/group.js b/src/models/group.js index 8207b1371..d54110c95 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -165,14 +165,33 @@ groupSchema.statics.getAllPublicGroups = function (callback) { return q.exec(callback) } -groupSchema.statics.getGroups = function (groupIds, callback) { - if (_.isUndefined(groupIds)) return callback('Invalid Array of Group IDs - GroupSchema.GetGroups()') - - this.model(COLLECTION) - .find({ _id: { $in: groupIds } }) - .populate('members', '_id username fullname email role preferences image title deleted') - .sort('name') - .exec(callback) +groupSchema.statics.getGroups = async function (groupIds, callback) { + return new Promise((resolve, reject) => { + ;(async () => { + if (_.isUndefined(groupIds)) { + if (typeof callback === 'function') return callback('Invalid Array of Group IDs - GroupSchema.GetGroups()') + return reject(new Error('Invalid Array of Group IDs - GroupSchema.GetGroups()')) + } + + try { + const exec = this.model(COLLECTION) + .find({ _id: { $in: groupIds } }) + .populate('members', '_id username fullname email role preferences image title deleted') + .sort('name') + + if (typeof callback === 'function') { + return exec.exec(callback) + } + + const groups = await exec.exec() + + return resolve(groups) + } catch (e) { + if (typeof callback === 'function') return callback(e) + return reject(e) + } + })() + }) } groupSchema.statics.getAllGroupsOfUser = function (userId, callback) { diff --git a/src/models/team.js b/src/models/team.js index 8c56349c3..43527e1e3 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -45,17 +45,22 @@ teamSchema.pre('save', function (next) { return next() }) -teamSchema.methods.addMember = function (memberId, callback) { - if (_.isUndefined(memberId)) return callback('Invalid MemberId - TeamSchema.AddMember()') +teamSchema.methods.addMember = async function (memberId, callback) { + return new Promise((resolve, reject) => { + ;(async () => { + if (_.isUndefined(memberId)) { + if (typeof callback === 'function') return callback({ message: 'Invalid MemberId - TeamSchema.AddMember()' }) + return reject(new Error('Invalid MemberId - TeamSchema.AddMember()')) + } - if (this.members === null) this.members = [] + if (this.members === null) this.members = [] - if (isMember(this.members, memberId)) return callback(null, false) + this.members.push(memberId) + this.members = _.uniq(this.members) - this.members.push(memberId) - this.members = _.uniq(this.members) - - return callback(null, true) + return resolve(true) + })() + }) } teamSchema.methods.removeMember = function (memberId, callback) { diff --git a/src/public/js/angularjs/controllers/profile.js b/src/public/js/angularjs/controllers/profile.js index 2396cd364..704ec532c 100644 --- a/src/public/js/angularjs/controllers/profile.js +++ b/src/public/js/angularjs/controllers/profile.js @@ -91,8 +91,13 @@ define([ }) }) .error(function (e) { - $log.log('[trudesk:profile:updateUser] - ' + e.error.message) - helpers.UI.showSnackbar('Error ' + e.error.message, true) + if (e.error.message) { + $log.log('[trudesk:profile:updateUser] - ' + e.error.message) + helpers.UI.showSnackbar('Error ' + e.error.message, true) + } else { + $log.log('[trudesk:profile:updateUser] - ' + e.error) + helpers.UI.showSnackbar('Error: ' + e.error, true) + } }) } diff --git a/src/public/js/vendor/formvalidator/jquery.form-validator.js b/src/public/js/vendor/formvalidator/jquery.form-validator.js index 782074f9b..146234286 100644 --- a/src/public/js/vendor/formvalidator/jquery.form-validator.js +++ b/src/public/js/vendor/formvalidator/jquery.form-validator.js @@ -2061,6 +2061,13 @@ // errorMessageKey: '' // not used }) + $.formUtils.addValidator({ + name: 'none', + validatorFunction: function(value, $el, config, language, $form) { + return true + } + }) + $.formUtils.addValidator({ name: 'confirmation', validatorFunction: function (value, $el, config, language, $form) { diff --git a/src/routes/index.js b/src/routes/index.js index 74d1b0f23..54a84c277 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -283,6 +283,7 @@ function mainRoutes (router, middleware, controllers) { router.get('/settings', middleware.redirectToLogin, middleware.loadCommonData, controllers.settings.general) router.get('/settings/general', middleware.redirectToLogin, middleware.loadCommonData, controllers.settings.general) + router.get('/settings/accounts', middleware.redirectToLogin, middleware.loadCommonData, controllers.settings.accounts) router.get( '/settings/appearance', middleware.redirectToLogin, diff --git a/src/settings/passwordComplexity.js b/src/settings/passwordComplexity.js new file mode 100644 index 000000000..d14ea39bd --- /dev/null +++ b/src/settings/passwordComplexity.js @@ -0,0 +1,50 @@ +/* + * . .o8 oooo + * .o8 "888 `888 + * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo + * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P' + * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888. + * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b. + * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o + * ======================================================================== + * Author: Chris Brame + * Updated: 5/17/22 6:33 PM + * Copyright (c) 2014-2022. All rights reserved. + */ + +const { passwordStrength } = require('check-password-strength') +const passwordComplexityOptions = [ + { + id: 0, + value: 'Too weak', + minDiversity: 0, + minLength: 0 + }, + { + id: 1, + value: 'Weak', + minDiversity: 2, + minLength: 6 + }, + { + id: 2, + value: 'Medium', + minDiversity: 3, + minLength: 8 + }, + { + id: 3, + value: 'Strong', + minDiversity: 4, + minLength: 10 + } +] + +const passwordComplexity = {} + +passwordComplexity.validate = password => { + const response = passwordStrength(password, passwordComplexityOptions) + return !(response.id === 0 || response.id === 1) +} + +module.exports = passwordComplexity diff --git a/src/settings/settingsUtil.js b/src/settings/settingsUtil.js index af6674ce9..b42728c12 100644 --- a/src/settings/settingsUtil.js +++ b/src/settings/settingsUtil.js @@ -122,6 +122,8 @@ util.getSettings = async callback => { s.maintenanceMode = parseSetting(settings, 'maintenanceMode:enable', false) + s.accountsPasswordComplexity = parseSetting(settings, 'accountsPasswordComplexity:enable', true) + const types = await ticketTypeSchema.getTypes() content.data.ticketTypes = _.sortBy(types, o => o.name) diff --git a/yarn.lock b/yarn.lock index 7d6b68cba..7e10022fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3878,6 +3878,11 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= +check-password-strength@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.5.tgz#bb10da01d24bd69e5e629c5cea2a6b729e5061af" + integrity sha512-b61T/+4OIGWSMRxJUsYOY44Cf9w7orIt2OQmF/WgH16qbJKIT1jG3XHx3jP+o090eH7rq13DRleKgXCiROBzMQ== + cheerio-select@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823"