'use strict'; /* global angular */ /* global Clipboard */ /* global async */ /* global ROLES */ /* global $ */ angular.module('Application').controller('UsersController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) { Client.onReady(function () { if (!Client.getUserInfo().isAtLeastUserManager) $location.path('/'); }); $scope.ready = false; $scope.users = []; // users of current page $scope.allUsersById = []; $scope.groups = []; $scope.hasLocalGroups = false; $scope.groupsById = { }; $scope.config = Client.getConfig(); $scope.userInfo = Client.getUserInfo(); $scope.roles = []; $scope.allUsers = []; // all the users and not just current page, have to load this for group assignment $scope.userSearchString = ''; $scope.currentPage = 1; $scope.pageItems = localStorage.cloudronPageSize || 15; $scope.userRefreshBusy = true; $scope.userStates = [ { state: 'ALL', value: null, label: 'All Users' }, { state: 'ACTIVE', value: true, label: 'Active Users' }, { state: 'INACTIVE', value: false, label: 'Inactive Users' } ]; $translate(['users.stateFilter.all', 'users.stateFilter.active', 'users.stateFilter.inactive']).then(function (tr) { if (tr['users.stateFilter.all']) $scope.userStates.find(function (a) { return a.state === 'ALL'; }).label = tr['users.stateFilter.all']; if (tr['users.stateFilter.active']) $scope.userStates.find(function (a) { return a.state === 'ACTIVE'; }).label = tr['users.stateFilter.active']; if (tr['users.stateFilter.inactive']) $scope.userStates.find(function (a) { return a.state === 'INACTIVE'; }).label = tr['users.stateFilter.inactive']; }); $scope.userStateFilter = $scope.userStates[0]; $scope.$watch('userStateFilter', function (newVal, oldVal) { if (newVal === oldVal) return; $scope.updateFilter(); }); $scope.groupMembers = function (group) { return group.userIds.filter(function (uid) { return !!$scope.allUsersById[uid]; }).map(function (uid) { return $scope.allUsersById[uid].username || $scope.allUsersById[uid].email; }).join(' '); }; $scope.canEdit = function (user) { let roleInt1 = $scope.roles.findIndex(function (role) { return role.id === $scope.userInfo.role; }); let roleInt2 = $scope.roles.findIndex(function (role) { return role.id === user.role; }); return (roleInt1 - roleInt2) >= 0; }; $scope.canImpersonate = function (user) { // only admins can impersonate if (!$scope.userInfo.isAtLeastAdmin) return false; // only users with username can be impersonated if (!user.username) return false; // normal admins cannot impersonate owners if (!$scope.userInfo.isAtLeastOwner && [ ROLES.OWNER ].indexOf(user.role) !== -1) return false; return true; }; $scope.userRemove = { busy: false, error: null, userInfo: {}, show: function (userInfo) { $scope.userRemove.error = null; $scope.userRemove.userInfo = userInfo; $('#userRemoveModal').modal('show'); }, submit: function () { $scope.userRemove.busy = true; Client.removeUser($scope.userRemove.userInfo.id, function (error) { $scope.userRemove.busy = false; if (error && error.statusCode === 403) return $scope.userRemove.error = error.message; else if (error) return console.error('Unable to delete user.', error); $scope.userRemove.userInfo = {}; refreshCurrentPage(); refreshAllUsers(); $('#userRemoveModal').modal('hide'); }); } }; $scope.userAdd = { busy: false, alreadyTaken: false, error: {}, email: '', fallbackEmail: '', username: '', displayName: '', selectedLocalGroups: [], role: 'user', sendInvite: false, show: function () { $scope.userAdd.error = {}; $scope.userAdd.email = ''; $scope.userAdd.fallbackEmail = ''; $scope.userAdd.username = ''; $scope.userAdd.displayName = ''; $scope.userAdd.selectedLocalGroups = []; $scope.userAdd.role = 'user'; $scope.userAdd.sendInvite = false; $scope.useraddForm.$setUntouched(); $scope.useraddForm.$setPristine(); $('#userAddModal').modal('show'); }, submit: function () { $scope.userAdd.busy = true; $scope.userAdd.alreadyTaken = false; $scope.userAdd.error.email = null; $scope.userAdd.error.fallbackEmail = null; $scope.userAdd.error.username = null; $scope.userAdd.error.displayName = null; var user = { username: $scope.userAdd.username || null, email: $scope.userAdd.email, fallbackEmail: $scope.userAdd.fallbackEmail, displayName: $scope.userAdd.displayName, role: $scope.userAdd.role }; Client.addUser(user, function (error, userId) { if (error) { $scope.userAdd.busy = false; if (error.statusCode === 409) { if (error.message.toLowerCase().indexOf('email') !== -1) { $scope.userAdd.error.email = 'Email already taken'; $scope.useraddForm.email.$setPristine(); $('#inputUserAddEmail').focus(); } else if (error.message.toLowerCase().indexOf('username') !== -1 || error.message.toLowerCase().indexOf('mailbox') !== -1) { $scope.userAdd.error.username = 'Username already taken'; $scope.useraddForm.username.$setPristine(); $('#inputUserAddUsername').focus(); } else { // should not happen!! console.error(error.message); } return; } else if (error.statusCode === 400) { if (error.message.toLowerCase().indexOf('email') !== -1) { $scope.userAdd.error.email = 'Invalid Email'; $scope.userAdd.error.emailAttempted = $scope.userAdd.email; $scope.useraddForm.email.$setPristine(); $('#inputUserAddEmail').focus(); } else if (error.message.toLowerCase().indexOf('username') !== -1) { $scope.userAdd.error.username = error.message; $scope.useraddForm.username.$setPristine(); $('#inputUserAddUsername').focus(); } else { console.error('Unable to create user.', error.statusCode, error.message); } return; } else { return console.error('Unable to create user.', error.statusCode, error.message); } } var localGroupIds = $scope.userAdd.selectedLocalGroups.map(function (g) { return g.id; }); Client.setLocalGroups(userId, localGroupIds, function (error) { $scope.userAdd.busy = false; if (error) return console.error(error); if ($scope.userAdd.sendInvite) Client.sendInviteEmail(userId, user.email, function (error) { if (error) console.error('Failed to send invite.', error); }); refreshCurrentPage(); refreshAllUsers(); $('#userAddModal').modal('hide'); }); }); } }; $scope.userEdit = { busy: false, reset2FABusy: false, error: {}, userInfo: {}, // form fields username: '', email: '', fallbackEmail: '', aliases: {}, displayName: '', active: false, source: '', selectedLocalGroups: [], externalGroups: [], role: '', show: function (userInfo) { $scope.userEdit.error = {}; $scope.userEdit.username = userInfo.username; $scope.userEdit.email = userInfo.email; $scope.userEdit.displayName = userInfo.displayName; $scope.userEdit.fallbackEmail = userInfo.fallbackEmail; $scope.userEdit.userInfo = userInfo; $scope.userEdit.selectedLocalGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; }).filter(function (g) { return g.source === ''; }); $scope.userEdit.externalGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; }).filter(function (g) { return g.source !== ''; }); $scope.userEdit.active = userInfo.active; $scope.userEdit.source = userInfo.source; $scope.userEdit.role = userInfo.role; $scope.useredit_form.$setPristine(); $scope.useredit_form.$setUntouched(); $('#userEditModal').modal('show'); }, submit: function () { $scope.userEdit.error = {}; $scope.userEdit.busy = true; var userId = $scope.userEdit.userInfo.id; async.series([ function setRole(next) { if (userId === $scope.userInfo.id) return next(); // cannot set role on self Client.setRole(userId, $scope.userEdit.role, next); }, function setActive(next) { if (userId === $scope.userInfo.id) return next(); // cannot set role on self Client.setActive(userId, $scope.userEdit.active, next); }, function updateUserProfile(next) { if ($scope.userEdit.source) return next(); // cannot update profile of external user // username is settable only if it was empty previously. it's editable for the "lock" profiles feature var data = {}; if (!$scope.userEdit.userInfo.username) data.username = $scope.userEdit.username; data.email = $scope.userEdit.email; data.displayName = $scope.userEdit.displayName; data.fallbackEmail = $scope.userEdit.fallbackEmail; Client.updateUserProfile(userId, data, next); }, function setLocalGroups(next) { var localGroupIds = $scope.userEdit.selectedLocalGroups.map(function (g) { return g.id; }); Client.setLocalGroups(userId, localGroupIds, next); } ], function (error) { $scope.userEdit.busy = false; if (error) { if (error.statusCode === 409) { if (error.message.toLowerCase().indexOf('email') !== -1) { $scope.userEdit.error.email = 'Email already taken'; } else if (error.message.toLowerCase().indexOf('username') !== -1) { $scope.userEdit.error.username = 'Username already taken'; } $scope.useredit_form.email.$setPristine(); $('#inputUserEditEmail').focus(); } else { $scope.userEdit.error.generic = error.message; console.error('Unable to update user:', error); } return; } refreshUsersCurrentPage(false /* busy indicator */); refreshGroups(); $('#userEditModal').modal('hide'); }); }, reset2FA: function () { $scope.userEdit.reset2FABusy = true; Client.disableTwoFactorAuthenticationByUserId($scope.userEdit.userInfo.id, function (error) { if (error) return console.error(error); $timeout(function () { $scope.userEdit.userInfo.twoFactorAuthenticationEnabled = false; $scope.userEdit.reset2FABusy = false; }, 3000); }); } }; $scope.groupAdd = { busy: false, error: {}, name: '', selectedUsers: [], show: function () { $scope.groupAdd.busy = false; $scope.groupAdd.error = {}; $scope.groupAdd.name = ''; $scope.groupAdd.selectedUsers = []; $scope.groupAddForm.$setUntouched(); $scope.groupAddForm.$setPristine(); $('#groupAddModal').modal('show'); }, submit: function () { $scope.groupAdd.busy = true; $scope.groupAdd.error = {}; Client.createGroup($scope.groupAdd.name, function (error, result) { if (error) { $scope.groupAdd.busy = false; if (error.statusCode === 409) { $scope.groupAdd.error.name = 'Name already taken'; $scope.groupAddForm.name.$setPristine(); $('#groupAddName').focus(); return; } else if (error.statusCode === 400) { $scope.groupAdd.error.name = error.message; $scope.groupAddForm.name.$setPristine(); $('#groupAddName').focus(); return; } else { return console.error('Unable to create group.', error.statusCode, error.message); } } var userIds = $scope.groupAdd.selectedUsers.map(function (u) { return u.id; }); Client.setGroupMembers(result.id, userIds, function (error) { $scope.groupAdd.busy = false; if (error) return console.error('Unable to add memebers.', error.statusCode, error.message); refreshCurrentPage(); $('#groupAddModal').modal('hide'); }); }); } }; $scope.groupEdit = { busy: false, error: {}, groupInfo: {}, name: '', source: '', selectedUsers: [], selectedApps: [], selectedAppsOriginal: [], apps: [], show: function (groupInfo) { $scope.groupEdit.error = {}; $scope.groupEdit.groupInfo = groupInfo; $scope.groupEdit.name = groupInfo.name; $scope.groupEdit.source = groupInfo.source; $scope.groupEdit.selectedUsers = groupInfo.userIds.map(function (uid) { return $scope.allUsersById[uid]; }); $scope.groupEdit.apps = Client.getInstalledApps(); $scope.groupEdit.selectedApps = Client.getInstalledApps().filter(function (app) { if (app.accessRestriction === null || !Array.isArray(app.accessRestriction.groups)) return false; return app.accessRestriction.groups.indexOf(groupInfo.id) !== -1; }); angular.copy($scope.groupEdit.selectedApps, $scope.groupEdit.selectedAppsOriginal); $scope.groupEdit_form.$setPristine(); $scope.groupEdit_form.$setUntouched(); $('#groupEditModal').modal('show'); }, updateAccessRestriction: function () { // find apps where ACL has changed var addedApps = $scope.groupEdit.selectedApps.filter(function (a) { return !$scope.groupEdit.selectedAppsOriginal.find(function (b) { return b.id === a.id; }); }); var removedApps = $scope.groupEdit.selectedAppsOriginal.filter(function (a) { return !$scope.groupEdit.selectedApps.find(function (b) { return b.id === a.id; }); }); async.eachSeries(addedApps, function (app, callback) { var accessRestriction = app.accessRestriction; if (!accessRestriction) accessRestriction = { users: [], groups: [] }; if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = []; accessRestriction.groups.push($scope.groupEdit.groupInfo.id); Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback); }, function (error) { if (error) { $scope.groupEdit.busy = false; return console.error('Unable to set added app access.', error.statusCode, error.message); } async.eachSeries(removedApps, function (app, callback) { var accessRestriction = app.accessRestriction; if (!accessRestriction) accessRestriction = { users: [], groups: [] }; if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = []; var deleted = accessRestriction.groups.splice(accessRestriction.groups.indexOf($scope.groupEdit.groupInfo.id), 1); // if not found return early if (deleted.length === 0) return callback(); Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback); }, function (error) { $scope.groupEdit.busy = false; if (error) return console.error('Unable to set removed app access.', error.statusCode, error.message); refreshCurrentPage(); // refresh apps to reflect change Client.refreshInstalledApps(); $('#groupEditModal').modal('hide'); }); }); }, submit: function () { $scope.groupEdit.busy = true; $scope.groupEdit.error = {}; if ($scope.groupEdit.source) return $scope.groupEdit.updateAccessRestriction(); // cannot update name or members of external groups Client.setGroupName($scope.groupEdit.groupInfo.id, $scope.groupEdit.name, function (error) { if (error) { $scope.groupEdit.busy = false; if (error.statusCode === 409) { $scope.groupEdit.error.name = 'Name already taken'; $scope.groupEditForm.name.$setPristine(); $('#groupEditName').focus(); return; } else if (error.statusCode === 400) { $scope.groupEdit.error.name = error.message; $scope.groupEditForm.name.$setPristine(); $('#groupEditName').focus(); return; } else { return console.error('Unable to edit group.', error.statusCode, error.message); } } var userIds = $scope.groupEdit.selectedUsers.map(function (u) { return u.id; }); Client.setGroupMembers($scope.groupEdit.groupInfo.id, userIds, function (error) { if (error) { $scope.groupEdit.busy = false; return console.error('Unable to set group members.', error.statusCode, error.message); } $scope.groupEdit.updateAccessRestriction(); }); }); } }; $scope.groupRemove = { busy: false, error: {}, group: null, memberCount: 0, show: function (group) { $scope.groupRemove.busy = false; $scope.groupRemove.error = {}; $scope.groupRemove.group = angular.copy(group); Client.getGroup(group.id, function (error, result) { if (error) return console.error('Unable to fetch group information.', error.statusCode, error.message); $scope.groupRemove.memberCount = result.userIds.length; $('#groupRemoveModal').modal('show'); }); }, submit: function () { $scope.groupRemove.busy = true; $scope.groupRemove.error = {}; Client.removeGroup($scope.groupRemove.group.id, function (error) { $scope.groupRemove.busy = false; if (error) return console.error('Unable to remove group.', error.statusCode, error.message); refreshCurrentPage(); $('#groupRemoveModal').modal('hide'); }); } }; $scope.isMe = function (user) { return user.username === Client.getUserInfo().username; }; $scope.passwordReset = { busy: false, resetLink: '', user: null, email: '', emailError: null, show: function (user) { $scope.passwordReset.busy = false; $scope.passwordReset.resetLink = ''; $scope.passwordReset.user = user; $scope.passwordReset.email = user.fallbackEmail || user.email; $scope.passwordReset.emailError = null; Client.getPasswordResetLink(user.id, function (error, result) { if (error) return console.error('Failed to get password reset link.', error); $scope.passwordReset.resetLink = result.passwordResetLink; $('#passwordResetModal').modal('show'); }); }, sendEmail: function () { $scope.passwordReset.busy = true; $scope.passwordReset.emailError = null; Client.sendPasswordResetEmail($scope.passwordReset.user.id, $scope.passwordReset.email, function (error) { $scope.passwordReset.busy = false; if (error) { $scope.passwordReset.emailError = error.message; } else { $scope.passwordReset.emailError = ''; } }); } }; $scope.invitation = { busy: false, inviteLink: '', user: null, email: '', show: function (user) { $scope.invitation.busy = false; $scope.invitation.inviteLink = ''; $scope.invitation.user = user; $scope.invitation.email = user.email; Client.getInviteLink(user.id, function (error, result) { if (error) return console.error('Failed to get invite link.', error); $scope.invitation.inviteLink = result.inviteLink; $('#invitationModal').modal('show'); }); }, sendEmail: function () { $scope.invitation.busy = true; Client.sendInviteEmail($scope.invitation.user.id, $scope.invitation.email, function (error) { if (error) return console.error('Failed to send invite email.', error); $scope.invitation.busy = false; Client.notify($translate.instant('users.invitationNotification.title'), $translate.instant('users.invitationNotification.body', { email: $scope.invitation.email }), false, 'success'); }); } }; // https://stackoverflow.com/questions/1497481/javascript-password-generator function generatePassword() { var length = 12, charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', retVal = ''; for (var i = 0, n = charset.length; i < length; ++i) { retVal += charset.charAt(Math.floor(Math.random() * n)); } return retVal; } $scope.setGhost = { busy: false, error: null, success: false, user: null, password: '', show: function (user) { $scope.setGhost.busy = false; $scope.setGhost.success = false; $scope.setGhost.error = null; $scope.setGhost.user = user; $scope.setGhost.password = ''; $('#setGhostModal').modal('show'); }, generatePassword: function () { $scope.setGhost.password = generatePassword(); }, submit: function () { $scope.setGhost.busy = true; Client.setGhost($scope.setGhost.user.id, $scope.setGhost.password, null, function (error) { $scope.setGhost.busy = false; if (error) { $scope.setGhost.error = error.message; return console.error(error); } $scope.setGhost.success = true; }); } }; function getUsersCurrentPage(callback) { var users = []; Client.getUsers($scope.userSearchString, $scope.userStateFilter.value, $scope.currentPage, $scope.pageItems, function (error, results) { if (error) return console.error(error); async.eachOf(results, function (result, index, iteratorDone) { Client.getUser(result.id, function (error, user) { if (error) return iteratorDone(error); users[index] = user; // keep the sorting order iteratorDone(); }); }, function (error) { callback(error, users); }); }); } function refreshUsersCurrentPage(showBusy) { // loads users on current page only if (showBusy) $scope.userRefreshBusy = true; getUsersCurrentPage(function (error, result) { if (error) return console.error('Unable to get user listing.', error); angular.copy(result, $scope.users); $scope.ready = true; $scope.userRefreshBusy = false; }); } function refreshGroups(callback) { Client.getGroups(function (error, result) { if (error) { if (callback) return callback(error); else return console.error('Unable to get group listing.', error); } angular.copy(result, $scope.groups); $scope.groupsById = { }; $scope.hasLocalGroups = false; for (var i = 0; i < result.length; i++) { $scope.groupsById[result[i].id] = result[i]; if (result[i].source === '') $scope.hasLocalGroups = true; } if (callback) callback(); }); } function refreshCurrentPage() { refreshGroups(function (error) { if (error) return console.error('Unable to get group listing.', error); refreshUsersCurrentPage(true /* busy indicator */); }); } $scope.showNextPage = function () { $scope.currentPage++; refreshUsersCurrentPage(false /* no busy indicator */); }; $scope.showPrevPage = function () { if ($scope.currentPage > 1) $scope.currentPage--; else $scope.currentPage = 1; refreshUsersCurrentPage(false /* no busy indicator */); }; $scope.updateFilter = function () { refreshUsersCurrentPage(false /* no busy indicator */); }; function refreshAllUsers() { // this loads all users on Cloudron, not just current page Client.getAllUsers(function (error, results) { if (error) return console.error(error); $scope.allUsers = results; $scope.allUsersById = {}; for (var i = 0; i < results.length; i++) { $scope.allUsersById[results[i].id] = results[i]; } }); } Client.onReady(function () { refreshCurrentPage(); refreshAllUsers(); // Order matters for permissions used in canEdit $scope.roles = [ { id: 'user', name: $translate.instant('users.role.user'), disabled: false }, { id: 'usermanager', name: $translate.instant('users.role.usermanager'), disabled: false }, { id: 'mailmanager', name: $translate.instant('users.role.mailmanager'), disabled: false }, { id: 'admin', name: $translate.instant('users.role.admin'), disabled: !$scope.user.isAtLeastAdmin }, { id: 'owner', name: $translate.instant('users.role.owner'), disabled: !$scope.user.isAtLeastOwner } ]; // give search the initial focus setTimeout(function () { $('#userSearchInput').focus(); }, 1); }); // setup all the dialog focus handling ['userAddModal', 'userRemoveModal', 'userEditModal', 'groupAddModal', 'groupEditModal', 'groupRemoveModal'].forEach(function (id) { $('#' + id).on('shown.bs.modal', function () { $(this).find('[autofocus]:first').focus(); }); }); new Clipboard('#passwordResetLinkClipboardButton').on('success', function(e) { $('#passwordResetLinkClipboardButton').tooltip({ title: 'Copied!', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#passwordResetLinkClipboardButton').tooltip('hide'); }, 2000); e.clearSelection(); }).on('error', function(/*e*/) { $('#passwordResetLinkClipboardButton').tooltip({ title: 'Press Ctrl+C to copy', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#passwordResetLinkClipboardButton').tooltip('hide'); }, 2000); }); new Clipboard('#invitationLinkClipboardButton').on('success', function(e) { $('#invitationLinkClipboardButton').tooltip({ title: 'Copied!', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#invitationLinkClipboardButton').tooltip('hide'); }, 2000); e.clearSelection(); }).on('error', function(/*e*/) { $('#invitationLinkClipboardButton').tooltip({ title: 'Press Ctrl+C to copy', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#invitationLinkClipboardButton').tooltip('hide'); }, 2000); }); new Clipboard('#setGhostClipboardButton').on('success', function(e) { $('#setGhostClipboardButton').tooltip({ title: 'Copied!', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#setGhostClipboardButton').tooltip('hide'); }, 2000); e.clearSelection(); }).on('error', function(/*e*/) { $('#setGhostClipboardButton').tooltip({ title: 'Press Ctrl+C to copy', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#setGhostClipboardButton').tooltip('hide'); }, 2000); }); $('.modal-backdrop').remove(); }]);