CVE-2022-1775 (published 2022-05-20T23:15Z)

Weak Password Requirements in GitHub repository polonel/trudesk prior to 1.2.2.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-521
cwe_name: Weak Password Requirements
description: The product does not require that users should have strong passwords, which makes it easier for attackers to compromise user accounts.
extended_description: Authentication mechanisms often rely on a memorized secret (also known as a password) to provide an assertion of identity for a user of a system. It is therefore important that this password be of sufficient complexity and impractical for an adversary to guess. The specific requirements around how complex a password needs to be depends on the type of system being protected. Selecting the correct password requirements and enforcing them through implementation are critical to the overall success of the authentication mechanism.
         

Fix information:
repo_url: https://github.com/polonel/trudesk
commit_details: repo_url: https://github.com/polonel/trudesk
author: Chris Brame
author_date: 2022-05-18 00:18:45-04:00
msg: refactor(accounts): password security enhancements
repository: repo_url: https://github.com/polonel/trudesk
repo_name: polonel/trudesk
description:  :coffee: :seedling: Trudesk is an open-source help desk/ticketing solution.
repo_language: JavaScript
owner: polonel
file_changes: filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -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",

programming_language: JavaScript
filename: index.jsx
change_type: ModificationType.MODIFY
diff: @@ -280,6 +280,12 @@ class Sidebar extends React.Component {
                     href='/settings'
                     active={activeSubItem === 'settings-general'}
                   />
+                  <SubmenuItem
+                    text='Accounts'
+                    icon='tune'
+                    href='/settings/accounts'
+                    active={activeSubItem === 'settings-accounts'}
+                  />
                   <SubmenuItem
                     text='Appearance'
                     icon='style'

programming_language: CoffeeScript
filename: CreateAccountModal.jsx
change_type: ModificationType.MODIFY
diff: @@ -203,9 +203,9 @@ class CreateAccountModal extends React.Component {
                   name={'password_confirmation'}
                   value={this.password}
                   onChange={e => 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.'}
                 />
               </div>
               <div className='uk-float-left uk-width-1-2'>

programming_language: JavaScript
filename: index.jsx
change_type: ModificationType.ADD
diff: @@ -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 (
+      <div className={active ? 'active' : 'hide'}>
+        <SettingItem
+          title='Allow User Registration'
+          subtitle='Allow users to create accounts on the login screen.'
+          component={
+            <EnableSwitch
+              stateName='allowUserRegistration'
+              label='Enable'
+              checked={this.allowUserRegistrationEnabled}
+              onChange={e => {
+                this.updateSetting('allowUserRegistration', 'allowUserRegistration:enable', e.target.checked)
+              }}
+            />
+          }
+        />
+        <SettingItem
+          title={'Password Complexity'}
+          subtitle={'Require users passwords to meet minimum password complexity'}
+          tooltip={'Minimum 8 characters with uppercase and numeric.'}
+          component={
+            <EnableSwitch
+              stateName={'accountsPasswordComplexity'}
+              label={'Enable'}
+              checked={this.passwordComplexityEnabled}
+              onChange={e => {
+                this.updateSetting('accountsPasswordComplexity', 'accountsPasswordComplexity:enable', e.target.checked)
+              }}
+            />
+          }
+        />
+      </div>
+    )
+  }
+}
+
+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)

programming_language: JavaScript
filename: index.jsx
change_type: ModificationType.MODIFY
diff: @@ -94,17 +94,6 @@ class GeneralSettings extends React.Component {
       />
     )
 
-    const AllowUserRegistration = (
-      <EnableSwitch
-        stateName='allowUserRegistration'
-        label='Enable'
-        checked={this.getSettingsValue('allowUserRegistration')}
-        onChange={e => {
-          this.updateSetting('allowUserRegistration', 'allowUserRegistration:enable', e.target.checked)
-        }}
-      />
-    )
-
     return (
       <div className={active ? 'active' : 'hide'}>
         <SettingItem
@@ -184,11 +173,6 @@ class GeneralSettings extends React.Component {
             </ZoneBox>
           </Zone>
         </SettingItem>
-        <SettingItem
-          title='Allow User Registration'
-          subtitle='Allow users to create accounts on the login screen.'
-          component={AllowUserRegistration}
-        />
       </div>
     )
   }

programming_language: JavaScript
filename: SettingsContainer.jsx
change_type: ModificationType.MODIFY
diff: @@ -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')
                 }}
               />
+              <MenuItem
+                title='Accounts'
+                active={this.state.activeCategory === 'settings-accounts'}
+                onClick={e => {
+                  this.onMenuItemClick(e, 'accounts')
+                }}
+              />
               <MenuItem
                 title='Appearance'
                 active={this.state.activeCategory === 'settings-appearance'}
@@ -156,6 +164,7 @@ class SettingsContainer extends React.Component {
           <div className='page-wrapper full-height scrollable no-overflow-x' ref={i => (this.page = i)}>
             <div className='settings-wrap'>
               <GeneralSettings active={this.state.activeCategory === 'settings-general'} />
+              <AccountsSettings active={this.state.activeCategory === 'settings-accounts'} />
               <AppearanceSettings active={this.state.activeCategory === 'settings-appearance'} />
               <PermissionsSettingsContainer active={this.state.activeCategory === 'settings-permissions'} />
               <TicketsSettings active={this.state.activeCategory === 'settings-tickets'} />

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -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 })

programming_language: JavaScript
filename: apiUtils.js
change_type: ModificationType.MODIFY
diff: @@ -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')

programming_language: JavaScript
filename: users.js
change_type: ModificationType.MODIFY
diff: @@ -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
             }

programming_language: JavaScript
filename: accounts.js
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: JavaScript
filename: settings.js
change_type: ModificationType.MODIFY
diff: @@ -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('/')
 

programming_language: Lua
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -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)

programming_language: JavaScript
filename: department.js
change_type: ModificationType.MODIFY
diff: @@ -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)
+      }
+    })()
   })
 }
 

programming_language: JavaScript
filename: group.js
change_type: ModificationType.MODIFY
diff: @@ -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) {

programming_language: Lua
filename: team.js
change_type: ModificationType.MODIFY
diff: @@ -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) {

programming_language: Lua
filename: profile.js
change_type: ModificationType.MODIFY
diff: @@ -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)
+            }
           })
       }
 

programming_language: JavaScript
filename: jquery.form-validator.js
change_type: ModificationType.MODIFY
diff: @@ -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) {

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -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,

programming_language: CoffeeScript
filename: passwordComplexity.js
change_type: ModificationType.ADD
diff: @@ -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

programming_language: JavaScript
filename: settingsUtil.js
change_type: ModificationType.MODIFY
diff: @@ -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)
 

programming_language: Lua
filename: yarn.lock
change_type: ModificationType.MODIFY
diff: @@ -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"

programming_language: Shell