'use strict'; /* global Clipboard */ // poor man's async function asyncForEach(items, handler, callback) { var cur = 0; if (items.length === 0) return callback(); (function iterator() { handler(items[cur], function () { if (cur >= items.length-1) return callback(); ++cur; iterator(); }); })(); } angular.module('Application').controller('UsersController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) { Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); $scope.ready = false; $scope.users = []; $scope.groups = []; $scope.groupsById = { }; $scope.config = Client.getConfig(); $scope.userInfo = Client.getUserInfo(); $scope.emailDomains = []; // FIXME this needs a whole lot of rework as it is used for alias ui, which now needs to be multidomain aware $scope.mailConfig = { enabled: false }; $scope.userremove = { busy: false, error: {}, userInfo: {}, password: '', show: function (userInfo) { $scope.userremove.error.username = null; $scope.userremove.error.password = null; $scope.userremove.password = ''; $scope.userremove.userInfo = userInfo; $scope.userremove_form.$setPristine(); $scope.userremove_form.$setUntouched(); $('#userRemoveModal').modal('show'); }, submit: function () { $scope.userremove.error.password = null; $scope.userremove.busy = true; Client.removeUser($scope.userremove.userInfo.id, $scope.userremove.password, function (error) { $scope.userremove.busy = false; if (error && error.statusCode === 403) { $scope.userremove.error.password = 'Wrong password'; $scope.userremove.password = ''; $scope.userremove_form.password.$setPristine(); $('#inputUserRemovePassword').focus(); return; } if (error) return console.error('Unable to delete user.', error); $scope.userremove.userInfo = {}; $scope.userremove.password = ''; $scope.userremove_form.$setPristine(); $scope.userremove_form.$setUntouched(); refresh(); $('#userRemoveModal').modal('hide'); }); } }; $scope.useradd = { busy: false, alreadyTaken: false, error: {}, email: '', username: '', displayName: '', sendInvite: true, show: function () { $scope.useradd.error = {}; $scope.useradd.email = ''; $scope.useradd.username = ''; $scope.useradd.displayName = ''; $scope.useradd_form.$setUntouched(); $scope.useradd_form.$setPristine(); $('#userAddModal').modal('show'); }, submit: function () { $scope.useradd.busy = true; $scope.useradd.alreadyTaken = false; $scope.useradd.error.email = null; $scope.useradd.error.username = null; $scope.useradd.error.displayName = null; Client.createUser($scope.useradd.username || null, $scope.useradd.email, $scope.useradd.displayName, $scope.useradd.sendInvite, function (error) { $scope.useradd.busy = false; if (error && error.statusCode === 409) { if (error.message.toLowerCase().indexOf('email') !== -1) { $scope.useradd.error.email = 'Email already taken'; $scope.useradd_form.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.useradd_form.username.$setPristine(); $('#inputUserAddUsername').focus(); } else { // should not happen!! console.error(error.message); } return; } if (error && error.statusCode === 400) { if (error.message.toLowerCase().indexOf('email') !== -1) { $scope.useradd.error.email = 'Invalid Email'; $scope.useradd.error.emailAttempted = $scope.useradd.email; $scope.useradd_form.email.$setPristine(); $('#inputUserAddEmail').focus(); } else if (error.message.toLowerCase().indexOf('username') !== -1) { $scope.useradd.error.username = error.message; $scope.useradd_form.username.$setPristine(); $('#inputUserAddUsername').focus(); } else { console.error('Unable to create user.', error.statusCode, error.message); } return; } if (error) return console.error('Unable to create user.', error.statusCode, error.message); $scope.useradd.error = {}; $scope.useradd.email = ''; $scope.useradd.username = ''; $scope.useradd.displayName = ''; $scope.useradd_form.$setUntouched(); $scope.useradd_form.$setPristine(); refresh(); $('#userAddModal').modal('hide'); }); } }; $scope.useredit = { busy: false, busyFetching: false, error: {}, userInfo: {}, email: '', fallbackEmail: '', aliases: {}, emailAddresses: [], availableEmailAddresses: [], superuser: false, show: function (userInfo) { $scope.useredit.busyFetching = true; $scope.useredit.error = {}; $scope.useredit.email = userInfo.email; $scope.useredit.fallbackEmail = userInfo.fallbackEmail; $scope.useredit.userInfo = userInfo; $scope.useredit.groupIds = angular.copy(userInfo.groupIds); $scope.useredit.superuser = userInfo.groupIds.indexOf('admin') !== -1; $scope.useredit.availableEmailAddresses = $scope.emailDomains.map(function (d) { return userInfo.username + '@' + d.domain; }); $scope.useredit.emailAddresses = []; $scope.useredit.aliases = {}; // fetch user's mailboxes and aliases var tmp = []; asyncForEach($scope.emailDomains, function (domain, callback) { Client.getUserMailbox(domain.domain, userInfo.id, function (error) { if (error) return callback(); var emailAddress = userInfo.username + '@' + domain.domain; tmp.push(emailAddress); Client.getAliases(domain.domain, userInfo.id, function (error, result) { if (error) return callback(); $scope.useredit.aliases[emailAddress] = result.join(','); callback(); }); }); }, function (error) { $scope.useredit.busyFetching = false; if (error) return console.error(error); // we need this copy as angular multiselect cannot deal with dynamic arrays! $scope.useredit.emailAddresses = tmp; }); $scope.useredit_form.$setPristine(); $scope.useredit_form.$setUntouched(); // clear any alias error when the model changes. this is required because tagInput directive is not angular forms aware // http://blog.revolunet.com/blog/2013/11/28/create-resusable-angularjs-input-component/ has some notes on how to do that $scope.$watch('useredit.aliases', function () { $scope.useredit.error.aliases = null; }); $('#userEditModal').modal('show'); }, toggleGroup: function (group) { var pos = $scope.useredit.groupIds.indexOf(group.id); if (pos === -1) { $scope.useredit.groupIds.push(group.id); } else { $scope.useredit.groupIds.splice(pos, 1); } }, submit: function () { $scope.useredit.error = {}; $scope.useredit.busy = true; var data = { id: $scope.useredit.userInfo.id, email: $scope.useredit.email, fallbackEmail: $scope.useredit.fallbackEmail }; Client.updateUser(data, function (error) { if (error) { $scope.useredit.busy = false; if (error.statusCode === 409) { $scope.useredit.error.email = 'Email already taken'; $scope.useredit_form.email.$setPristine(); $('#inputUserEditEmail').focus(); } else { console.error('Unable to update user:', error); } return; } if ($scope.useredit.superuser) { if ($scope.useredit.groupIds.indexOf('admin') === -1) $scope.useredit.groupIds.push('admin'); } else { $scope.useredit.groupIds = $scope.useredit.groupIds.filter(function (groupId) { return groupId !== 'admin'; }); } Client.setGroups(data.id, $scope.useredit.groupIds, function (error) { if (error) return console.error('Unable to update groups for user:', error); asyncForEach($scope.useredit.availableEmailAddresses, function (address, callback) { var isEnabled = !!$scope.useredit.emailAddresses.find(function (a) { return a === address; }); var userId = $scope.useredit.userInfo.id; var domain = $scope.emailDomains.find(function (d) { return address.split('@')[1] === d.domain; }); if (!domain) console.error('Domain not found. Internal error should not happen.'); var aliases = $scope.useredit.aliases[address] ? $scope.useredit.aliases[address].split(',') : []; Client.setAliases(domain.domain, userId, aliases, function (error) { if (error) return callback(error); // TODO we could be smarter and check if the selection has actually changed if (isEnabled) Client.enableUserMailbox(domain.domain, userId, callback); else Client.disableUserMailbox(domain.domain, userId, callback); }); }, function (error) { $scope.useredit.busy = false; if (error) return console.error('unable to adjust email addresses.', error); $scope.useredit.userInfo = {}; $scope.useredit.email = ''; $scope.useredit.superuser = false; $scope.useredit.groupIds = []; $scope.useredit.emailAddresses = []; $scope.useredit.aliases = ''; $scope.useredit_form.$setPristine(); $scope.useredit_form.$setUntouched(); refresh(); $('#userEditModal').modal('hide'); }); }); }); } }; $scope.showBubble = function ($event) { $($event.target).tooltip('show'); setTimeout(function () { $($event.target).tooltip('hide'); }, 2000); }; $scope.groupAdd = { busy: false, error: {}, name: '', show: function () { $scope.groupAdd.busy = false; $scope.groupAdd.error = {}; $scope.groupAdd.name = ''; $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) { $scope.groupAdd.busy = false; if (error && error.statusCode === 409) { $scope.groupAdd.error.name = 'Name already taken'; $scope.groupAddForm.name.$setPristine(); $('#groupAddName').focus(); return; } if (error && error.statusCode === 400) { $scope.groupAdd.error.name = error.message; $scope.groupAddForm.name.$setPristine(); $('#groupAddName').focus(); return; } if (error) return console.error('Unable to create group.', error.statusCode, error.message); refresh(); $('#groupAddModal').modal('hide'); }); } }; $scope.inviteSent = { email: '', setupLink: '' }; $scope.groupEdit = { busy: false, busyFetching: false, error: {}, group: null, lists: [], availableLists: [], show: function (group) { $scope.groupEdit.busy = false; $scope.groupEdit.busyFetching = true; $scope.groupEdit.error = {}; $scope.groupEdit.availableLists = $scope.emailDomains.map(function (domain) { return { domain: domain, address: group.name + '@' + domain.domain }; }); $scope.groupEdit.selectedLists = []; $scope.groupEdit.currentLists = []; $scope.groupEdit.group = angular.copy(group); var tmp = []; asyncForEach($scope.emailDomains, function (domain, callback) { Client.getMailingList(domain.domain, $scope.groupEdit.group.id, function (error) { if (error) return callback(error); var list = $scope.groupEdit.availableLists.find(function (list) { return list.domain.domain === domain.domain; }); tmp.push(list); $scope.groupEdit.currentLists.push(list); callback(); }); }, function (error) { $scope.groupEdit.busyFetching = false; if (error) return console.error('Unable to get mailing lists.', error); $scope.groupEdit.selectedLists = tmp; }); $scope.groupEditForm.$setUntouched(); $scope.groupEditForm.$setPristine(); $('#groupEditModal').modal('show'); }, submit: function () { $scope.groupEdit.busy = true; var addedLists = $scope.groupEdit.selectedLists.filter(function (s) { return !$scope.groupEdit.currentLists.find(function (c) { return c.domain.domain === s.domain.domain; }); }); var removedLists = $scope.groupEdit.currentLists.filter(function (c) { return !$scope.groupEdit.selectedLists.find(function (s) { return s.domain.domain === c.domain.domain; }); }); asyncForEach(addedLists, function (list, callback) { Client.addMailingList(list.domain.domain, $scope.groupEdit.group.id, callback); }, function (error) { if (error) { $scope.groupEdit.busy = false; return console.error('Failed to add group to mailinglists.', error); } asyncForEach(removedLists, function (list, callback) { Client.removeMailingList(list.domain.domain, $scope.groupEdit.group.id, callback); }, function (error) { $scope.groupEdit.busy = false; if (error) { return console.error('Failed to remove group to mailinglists.', error); } $('#groupEditModal').modal('hide'); }); }); } }; $scope.groupRemove = { busy: false, error: {}, group: null, password: '', memberCount: 0, show: function (group) { $scope.groupRemove.busy = false; $scope.groupRemove.error = {}; $scope.groupRemove.password = ''; $scope.groupRemove.group = angular.copy(group); $scope.groupRemoveForm.$setUntouched(); $scope.groupRemoveForm.$setPristine(); 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, $scope.groupRemove.password, function (error) { $scope.groupRemove.busy = false; if (error && error.statusCode === 403) { $scope.groupRemove.error.password = 'Wrong password'; $scope.groupRemove.password = ''; $scope.groupRemoveForm.password.$setPristine(); $('#groupRemovePasswordInput').focus(); return; } if (error) return console.error('Unable to remove group.', error.statusCode, error.message); refresh(); $('#groupRemoveModal').modal('hide'); }); } }; $scope.isMe = function (user) { return user.username === Client.getUserInfo().username; }; $scope.isAdmin = function (user) { return !!user.admin; }; $scope.sendInvite = function (user) { $scope.inviteSent.email = user.fallbackEmail; $scope.inviteSent.setupLink = ''; Client.sendInvite(user, function (error, resetToken) { if (error) return console.error(error); // Client.notify('', 'Invitation was successfully sent to ' + user.email + '.', false, 'success'); $scope.inviteSent.setupLink = location.origin + '/api/v1/session/account/setup.html?reset_token=' + resetToken; $('#inviteSentModal').modal('show'); }); }; $scope.copyToClipboard = function (value) { document.execCommand('copy'); }; function refresh() { Client.getGroups(function (error, result) { if (error) return console.error('Unable to get group listing.', error); $scope.groups = result; $scope.groupsById = { }; for (var i = 0; i < result.length; i++) { $scope.groupsById[result[i].id] = result[i]; } Client.getUsers(function (error, result) { if (error) return console.error('Unable to get user listing.', error); $scope.users = result; Client.getDomains(function (error, result) { if (error) return console.error('Unable to get domain listing.', error); // reset so we can push the fresh config $scope.emailDomains = []; asyncForEach(result, function (domain, callback) { Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) { if (error) return callback(error); domain.mailConfig = mailConfig; // only collect domains where email is enabled if (mailConfig.enabled) $scope.emailDomains.push(domain); callback(); }); }, function (error) { if (error) return console.error('Unable to get mail config for domains.', error); $scope.ready = true; }); }); }); }); } Client.onReady(refresh); // setup all the dialog focus handling ['userAddModal', 'userRemoveModal', 'userEditModal', 'groupAddModal', 'groupRemoveModal'].forEach(function (id) { $('#' + id).on('shown.bs.modal', function () { $(this).find("[autofocus]:first").focus(); }); }); var clipboard = new Clipboard('#setupLinkButton'); clipboard.on('success', function(e) { $('#setupLinkButton').tooltip({ title: 'Copied!', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#setupLinkButton').tooltip('hide'); }, 2000); e.clearSelection(); }); clipboard.on('error', function(e) { $('#setupLinkButton').tooltip({ title: 'Press Ctrl+C to copy', trigger: 'manual' }).tooltip('show'); $timeout(function () { $('#setupLinkButton').tooltip('hide'); }, 2000); }); $('.modal-backdrop').remove(); }]);