Files
cloudron-box/dashboard/public/views/users.js
2024-10-04 14:30:44 +02:00

811 lines
30 KiB
JavaScript

'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();
}]);