diff --git a/dashboard/src/views/user-settings.html b/dashboard/src/views/user-settings.html new file mode 100644 index 000000000..c31c80849 --- /dev/null +++ b/dashboard/src/views/user-settings.html @@ -0,0 +1,956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+

+ {{ 'users.title' | tr }} +

+
+ +
+
+ + +
+ + +
+
+
+
+
+

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
{{ 'users.users.user' | tr }}{{ 'main.actions' | tr }}
{{ 'users.users.empty' | tr }}
+ + + + + + + {{ user.displayName }}   {{ user.username }}   + + {{ user.email }} + + + + + + + +
+
+
+ {{ 'users.users.count' | tr:{ count: allUsers.length } }} +
+
+ + {{ currentPage }} + +
+
+
+
+
+
+ +
+ +
+

+ {{ 'users.groups.title' | tr }} + +

+
+ +
+
+
+
+

+
+
+
+
+ + + + + + + + + + + + + + + +
{{ 'users.groups.name' | tr }}{{ 'main.actions' | tr }}
+ {{ group.name }}   + + {{ groupMembers(group) }} + + + +
+
+
+
+
+ +
+

{{ 'users.settings.title' | tr }}

+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+ +
+
+ {{ profileConfig.errorMessage }} + + +
+
+ +
+ +
+

{{ 'users.externalLdap.title' | tr }}

+
+ +
+
+
{{ 'users.externalLdap.description' | tr }}
+
+ +
+ +
+
+ {{ 'users.externalLdap.subscriptionRequired' | tr }} {{ 'users.externalLdap.subscriptionRequiredAction' | tr }} +
+
+ +
+ +
+
+ {{ 'users.externalLdap.noopInfo' | tr }} +
+
+ +
+
+ {{ 'users.externalLdap.provider' | tr }} +
+
+ {{ externalLdap.currentConfig.provider }} +
+
+ +
+
+ {{ 'users.externalLdap.server' | tr }} +
+
+ {{ externalLdap.currentConfig.url }} +
+
+ +
+
+ {{ 'users.externalLdap.acceptSelfSignedCert' | tr }} +
+
+ {{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }} +
+
+ +
+
+ {{ 'users.externalLdap.baseDn' | tr }} +
+
+ {{ externalLdap.currentConfig.baseDn }} +
+
+ +
+
+ {{ 'users.externalLdap.filter' | tr }} +
+
+ {{ externalLdap.currentConfig.filter }} +
+
+ +
+
+ {{ 'users.externalLdap.usernameField' | tr }} +
+
+ {{ externalLdap.currentConfig.usernameField || 'uid' }} +
+
+ +
+
+ {{ 'users.externalLdap.syncGroups' | tr }} +
+
+ {{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }} +
+
+ +
+
+ {{ 'users.externalLdap.groupBaseDn' | tr }} +
+
+ {{ externalLdap.currentConfig.groupBaseDn }} +
+
+ +
+
+ {{ 'users.externalLdap.groupFilter' | tr }} +
+
+ {{ externalLdap.currentConfig.groupFilter }} +
+
+ +
+
+ {{ 'users.externalLdap.groupnameField' | tr }} +
+
+ {{ externalLdap.currentConfig.groupnameField }} +
+
+ +
+
+ {{ 'users.externalLdap.auth' | tr }} +
+
+ {{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }} +
+
+ +
+
+ {{ 'users.externalLdap.autocreateUsersOnLogin' | tr }} +
+
+ {{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }} +
+
+ +
+
+
+
+
+
+
+
+ +
+
+

{{ externalLdap.message }}

+

+

{{ externalLdap.errorMessage }}
+

+
+ +
+ + + {{ 'users.externalLdap.showLogsAction' | tr }} +
+
+
+
+ +
+

{{ 'users.exposedLdap.title' | tr }}

+
+ +
+
+
+
{{ 'users.exposedLdap.description' | tr }}
+ +
+ +
+
+
+ +
+
+ +
+ + + + +
+
+
+ +

+ +
{{ userDirectoryConfig.error.secret }}
+
+
+ +

{{ 'users.exposedLdap.ipRestriction.description' | tr }}

+ +
{{ userDirectoryConfig.error.allowlist }}
+
+
+
+ +
+ +
+ {{ userDirectoryConfig.error.generic }} + + +
+
+
+
+ +
+

{{ 'oidc.title' | tr }}

+
+ +
+
+
+ {{ 'oidc.description' | tr }} + {{ 'main.settings' | tr }} +
+
+
+
diff --git a/dashboard/src/views/user-settings.js b/dashboard/src/views/user-settings.js new file mode 100644 index 000000000..1222ca49b --- /dev/null +++ b/dashboard/src/views/user-settings.js @@ -0,0 +1,1308 @@ +'use strict'; + +/* global angular */ +/* global Clipboard */ +/* global async */ +/* global ROLES */ +/* global $ */ + +angular.module('Application').controller('UserSettingsController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) { + Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); }); + + $scope.ldapProvider = [ + { name: 'Active Directory', value: 'ad' }, + { name: 'Cloudron', value: 'cloudron' }, + { name: 'Jumpcloud', value: 'jumpcloud' }, + { name: 'Okta', value: 'okta' }, + { name: 'Univention Corporate Server (UCS)', value: 'univention' }, + { name: 'Other', value: 'other' }, + { name: 'Disabled', value: 'noop' } + ]; + + $translate(['users.externalLdap.providerOther', 'users.externalLdap.providerDisabled']).then(function (tr) { + if (tr['users.externalLdap.providerOther']) $scope.ldapProvider.find(function (p) { return p.value === 'other'; }).name = tr['users.externalLdap.providerOther']; + if (tr['users.externalLdap.providerDisabled']) $scope.ldapProvider.find(function (p) { return p.value === 'noop'; }).name = tr['users.externalLdap.providerDisabled']; + }); + + $scope.ready = false; + $scope.users = []; // users of current page + $scope.allUsersById = []; + $scope.groups = []; + $scope.groupsById = { }; + $scope.config = Client.getConfig(); + $scope.userInfo = Client.getUserInfo(); + $scope.domains = []; + + $scope.openSubscriptionSetup = function () { + Client.openSubscriptionSetup($scope.$parent.subscription); + }; + + $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.userImport = { + busy: false, + done: false, + error: null, + percent: 0, + success: 0, + users: [], + sendInvite: false, + + reset: function () { + $scope.userImport.busy = false; + $scope.userImport.error = null; + $scope.userImport.users = []; + $scope.userImport.percent = 0; + $scope.userImport.success = 0; + $scope.userImport.done = false; + $scope.userImport.sendInvite = false; + }, + + handleFileChanged: function () { + $scope.userImport.reset(); + + var fileInput = document.getElementById('userImportFileInput'); + if (!fileInput.files || !fileInput.files[0]) return; + + var file = fileInput.files[0]; + if (file.type !== 'application/json' && file.type !== 'text/csv') return console.log('Unsupported file type.'); + + const reader = new FileReader(); + reader.addEventListener('load', function () { + $scope.$apply(function () { + $scope.userImport.users = []; + var users = []; + if (file.type === 'text/csv') { + var lines = reader.result.split('\n'); + if (lines.length === 0) return $scope.userImport.error = { file: 'Imported file has no lines' }; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line) continue; + var items = line.split(','); + if (items.length !== 5) { + $scope.userImport.error = { file: 'Line ' + (i+1) + ' has wrong column count. Expecting 5' }; + return; + } + users.push({ + username: items[0].trim(), + email: items[1].trim(), + fallbackEmail: items[2].trim(), + displayName: items[3].trim(), + role: items[4].trim() + }); + } + } else { + try { + users = JSON.parse(reader.result).map(function (user) { + return { + username: user.username, + email: user.email, + fallbackEmail: user.fallbackEmail, + displayName: user.displayName, + role: user.role + }; + }); + } catch (e) { + console.error('Failed to parse users.', e); + $scope.userImport.error = { file: 'Imported file is not valid JSON:' + e.message }; + } + } + $scope.userImport.users = users; + }); + }, false); + reader.readAsText(file); + }, + + show: function () { + $scope.userImport.reset(); + + // named so no duplactes + document.getElementById('userImportFileInput').addEventListener('change', $scope.userImport.handleFileChanged); + + $('#userImportModal').modal('show'); + }, + + openFileInput: function () { + $('#userImportFileInput').click(); + }, + + import: function () { + $scope.userImport.percent = 0; + $scope.userImport.success = 0; + $scope.userImport.done = false; + $scope.userImport.error = { import: [] }; + $scope.userImport.busy = true; + + var processed = 0; + + async.eachSeries($scope.userImport.users, function (user, callback) { + Client.addUser(user, function (error, userId) { + if (error) $scope.userImport.error.import.push({ error: error, user: user }); + else ++$scope.userImport.success; + + ++processed; + $scope.userImport.percent = 100 * processed / $scope.userImport.users.length; + + if (!error && $scope.userImport.sendInvite) { + console.log('sending', userId, user.email); + Client.sendInviteEmail(userId, user.email, function (error) { + if (error) console.error('Failed to send invite.', error); + }); + } + + callback(); + }); + }, function (error) { + if (error) return console.error(error); + + $scope.userImport.busy = false; + $scope.userImport.done = true; + if ($scope.userImport.success) { + refresh(); + refreshAllUsers(); + } + }); + } + }; + + // supported types are 'json' and 'csv' + $scope.userExport = function (type) { + Client.getAllUsers(function (error, result) { + if (error) { + Client.error('Failed to list users. Full error in the webinspector.'); + return console.error('Failed to list users.', error); + } + + var content = ''; + if (type === 'json') { + content = JSON.stringify(result.map(function (user) { + return { + id: user.id, + username: user.username, + email: user.email, + fallbackEmail: user.fallbackEmail, + displayName: user.displayName, + role: user.role, + active: user.active + }; + }), null, 2); + } else if (type === 'csv') { + content = result.map(function (user) { + return [ user.id, user.username, user.email, user.fallbackEmail, user.displayName, user.role, user.active ].join(','); + }).join('\n'); + } else { + return; + } + + var file = new Blob([ content ], { type: type === 'json' ? 'application/json' : 'text/csv' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(file); + a.download = type === 'json' ? 'users.json' : 'users.csv'; + document.body.appendChild(a); + a.click(); + }); + }; + + $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 = {}; + + refresh(); + refreshAllUsers(); + + $('#userRemoveModal').modal('hide'); + }); + } + }; + + $scope.useradd = { + busy: false, + alreadyTaken: false, + error: {}, + email: '', + fallbackEmail: '', + username: '', + displayName: '', + selectedGroups: [], + role: 'user', + sendInvite: false, + + show: function () { + $scope.useradd.error = {}; + $scope.useradd.email = ''; + $scope.useradd.fallbackEmail = ''; + $scope.useradd.username = ''; + $scope.useradd.displayName = ''; + $scope.useradd.selectedGroups = []; + $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 groupIds = $scope.useradd.selectedGroups.map(function (g) { return g.id; }); + + Client.setGroups(userId, groupIds, 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); }); + + refresh(); + refreshAllUsers(); + + $('#userAddModal').modal('hide'); + }); + }); + } + }; + + $scope.useredit = { + busy: false, + reset2FABusy: false, + error: {}, + userInfo: {}, + + // form fields + username: '', + email: '', + fallbackEmail: '', + aliases: {}, + displayName: '', + active: false, + source: '', + selectedGroups: [], + 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.selectedGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; }); + $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; + var data = { + id: userId + }; + + // only send if not the current active user + if (userId !== $scope.userInfo.id) { + data.active = $scope.useredit.active; + data.role = $scope.useredit.role; + } + + // only change those if it is a local user + if (!$scope.useredit.source) { + // username is settable only if it was empty previously. it's editable for the "lock" profiles feature + 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.updateUser(data, function (error) { + if (error) { + $scope.useredit.busy = false; + + 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; + } + + var groupIds = $scope.useredit.selectedGroups.map(function (g) { return g.id; }); + + Client.setGroups(data.id, groupIds, function (error) { + $scope.useredit.busy = false; + + if (error) return console.error('Unable to update groups for user:', error); + + refreshUsers(false); + + $('#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.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); + + refresh(); + + $('#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'); + }, + + submit: function () { + $scope.groupEdit.busy = true; + $scope.groupEdit.error = {}; + + Client.updateGroup($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); + } + + // 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); + + refresh(); + + // refresh apps to reflect change + Client.refreshInstalledApps(); + + $('#groupEditModal').modal('hide'); + }); + }); + }); + }); + } + }; + + $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); + + refresh(); + $('#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.makeLocal = { + busy: false, + user: null, + + show: function (user) { + $scope.makeLocal.busy = false; + $scope.makeLocal.user = user; + + $('#makeLocalModal').modal('show'); + }, + + submit: function () { + $scope.makeLocal.busy = false; + + Client.makeUserLocal($scope.makeLocal.user.id, function (error) { + if (error) return console.error('Failed to make user local.', error); + + $scope.makeLocal.busy = false; + + refreshUsers(); + + $('#makeLocalModal').modal('hide'); + }); + } + }; + + $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; + }); + } + }; + + $scope.profileConfig = { + editableUserProfiles: true, + mandatory2FA: false, + errorMessage: '', + + refresh: function () { + Client.getProfileConfig(function (error, result) { + if (error) return console.error('Unable to get directory config.', error); + + $scope.profileConfig.editableUserProfiles = !result.lockUserProfiles; + $scope.profileConfig.mandatory2FA = !!result.mandatory2FA; + }); + }, + + submit: function () { + // prevent the current user from getting locked out + if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) return Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile'); + + $scope.profileConfig.error = ''; + $scope.profileConfig.busy = true; + $scope.profileConfig.success = false; + + var data = { + lockUserProfiles: !$scope.profileConfig.editableUserProfiles, + mandatory2FA: $scope.profileConfig.mandatory2FA + }; + + Client.setProfileConfig(data, function (error) { + if (error) $scope.profileConfig.errorMessage = error.message; + + $scope.profileConfig.success = true; + + $scope.profileConfigForm.$setUntouched(); + $scope.profileConfigForm.$setPristine(); + + Client.refreshConfig(); // refresh the $scope.config + + $timeout(function () { + $scope.profileConfig.busy = false; + }, 500); + }); + } + }; + + $scope.userDirectoryConfig = { + enabled: false, + secret: '', + allowlist: '', + error: null, + + refresh: function () { + Client.getUserDirectoryConfig(function (error, result) { + if (error) return console.error('Unable to get exposed ldap config.', error); + + $scope.userDirectoryConfig.enabled = !!result.enabled; + $scope.userDirectoryConfig.allowlist = result.allowlist; + $scope.userDirectoryConfig.secret = result.secret; + }); + }, + + submit: function () { + $scope.userDirectoryConfig.error = null; + $scope.userDirectoryConfig.busy = true; + $scope.userDirectoryConfig.success = false; + + var data = { + enabled: $scope.userDirectoryConfig.enabled, + secret: $scope.userDirectoryConfig.secret, + allowlist: $scope.userDirectoryConfig.allowlist + }; + + Client.setUserDirectoryConfig(data, function (error) { + $scope.userDirectoryConfig.busy = false; + + if (error && error.statusCode === 400) { + if (error.message.indexOf('secret') !== -1) return $scope.userDirectoryConfig.error = { secret: error.message }; + else return $scope.userDirectoryConfig.error = { allowlist: error.message }; + } + if (error) return $scope.userDirectoryConfig.error = { generic: error.message }; + + $scope.userDirectoryConfigForm.$setUntouched(); + $scope.userDirectoryConfigForm.$setPristine(); + + $scope.userDirectoryConfig.success = true; + }); + } + }; + + $scope.externalLdap = { + busy: false, + percent: 0, + message: '', + errorMessage: '', + error: {}, + taskId: 0, + + syncBusy: false, + + // fields + provider: 'noop', + autoCreate: false, + url: '', + acceptSelfSignedCerts: false, + baseDn: '', + filter: '', + groupBaseDn: '', + bindDn: '', + bindPassword: '', + usernameField: '', + + currentConfig: {}, + + checkStatus: function () { + Client.getLatestTaskByType('syncExternalLdap', function (error, task) { + if (error) return console.error(error); + + if (!task) return; + + $scope.externalLdap.taskId = task.id; + $scope.externalLdap.updateStatus(); + }); + }, + + sync: function () { + $scope.externalLdap.syncBusy = true; + + Client.startExternalLdapSync(function (error, taskId) { + if (error) { + $scope.externalLdap.syncBusy = false; + console.error('Unable to start ldap syncer task.', error); + return; + } + + $scope.externalLdap.taskId = taskId; + $scope.externalLdap.updateStatus(); + }); + }, + + updateStatus: function () { + Client.getTask($scope.externalLdap.taskId, function (error, data) { + if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000); + + if (!data.active) { + $scope.externalLdap.syncBusy = false; + $scope.externalLdap.message = ''; + $scope.externalLdap.percent = 100; // indicates that 'result' is valid + $scope.externalLdap.errorMessage = data.success ? '' : data.error.message; + + refreshGroups(); + refreshUsers(); + + return; + } + + $scope.externalLdap.syncBusy = true; + $scope.externalLdap.percent = data.percent; + $scope.externalLdap.message = data.message; + window.setTimeout($scope.externalLdap.updateStatus, 3000); + }); + }, + + show: function () { + $scope.externalLdap.busy = false; + $scope.externalLdap.error = {}; + + $scope.externalLdap.provider = $scope.externalLdap.currentConfig.provider; + $scope.externalLdap.url = $scope.externalLdap.currentConfig.url; + $scope.externalLdap.acceptSelfSignedCerts = $scope.externalLdap.currentConfig.acceptSelfSignedCerts; + $scope.externalLdap.baseDn = $scope.externalLdap.currentConfig.baseDn; + $scope.externalLdap.filter = $scope.externalLdap.currentConfig.filter; + $scope.externalLdap.syncGroups = $scope.externalLdap.currentConfig.syncGroups; + $scope.externalLdap.groupBaseDn = $scope.externalLdap.currentConfig.groupBaseDn; + $scope.externalLdap.groupFilter = $scope.externalLdap.currentConfig.groupFilter; + $scope.externalLdap.groupnameField = $scope.externalLdap.currentConfig.groupnameField; + $scope.externalLdap.bindDn = $scope.externalLdap.currentConfig.bindDn; + $scope.externalLdap.bindPassword = $scope.externalLdap.currentConfig.bindPassword; + $scope.externalLdap.usernameField = $scope.externalLdap.currentConfig.usernameField; + $scope.externalLdap.autoCreate = $scope.externalLdap.currentConfig.autoCreate; + + $('#externalLdapModal').modal('show'); + }, + + submit: function () { + $scope.externalLdap.busy = true; + $scope.externalLdap.error = {}; + + var config = { + provider: $scope.externalLdap.provider + }; + + if ($scope.externalLdap.provider === 'cloudron') { + config.url = $scope.externalLdap.url; + config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts; + config.autoCreate = $scope.externalLdap.autoCreate; + config.syncGroups = $scope.externalLdap.syncGroups; + config.bindPassword = $scope.externalLdap.bindPassword; + + // those values are known and thus overwritten + config.baseDn = 'ou=users,dc=cloudron'; + config.filter = '(objectClass=inetOrgPerson)'; + config.usernameField = 'username'; + config.groupBaseDn = 'ou=groups,dc=cloudron'; + config.groupFilter = '(objectClass=group)'; + config.groupnameField = 'cn'; + config.bindDn = 'cn=admin,ou=system,dc=cloudron'; + } else if ($scope.externalLdap.provider !== 'noop') { + config.url = $scope.externalLdap.url; + config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts; + config.baseDn = $scope.externalLdap.baseDn; + config.filter = $scope.externalLdap.filter; + config.usernameField = $scope.externalLdap.usernameField; + config.syncGroups = $scope.externalLdap.syncGroups; + config.groupBaseDn = $scope.externalLdap.groupBaseDn; + config.groupFilter = $scope.externalLdap.groupFilter; + config.groupnameField = $scope.externalLdap.groupnameField; + config.autoCreate = $scope.externalLdap.autoCreate; + + if ($scope.externalLdap.bindDn) { + config.bindDn = $scope.externalLdap.bindDn; + config.bindPassword = $scope.externalLdap.bindPassword; + } + } + + Client.setExternalLdapConfig(config, function (error) { + $scope.externalLdap.busy = false; + + if (error) { + if (error.statusCode === 424) { + if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') $scope.externalLdap.error.acceptSelfSignedCerts = true; + else $scope.externalLdap.error.url = true; + } else if (error.statusCode === 400 && error.message === 'invalid baseDn') { + $scope.externalLdap.error.baseDn = true; + } else if (error.statusCode === 400 && error.message === 'invalid filter') { + $scope.externalLdap.error.filter = true; + } else if (error.statusCode === 400 && error.message === 'invalid groupBaseDn') { + $scope.externalLdap.error.groupBaseDn = true; + } else if (error.statusCode === 400 && error.message === 'invalid groupFilter') { + $scope.externalLdap.error.groupFilter = true; + } else if (error.statusCode === 400 && error.message === 'invalid groupnameField') { + $scope.externalLdap.error.groupnameField = true; + } else if (error.statusCode === 400 && error.message === 'invalid bind credentials') { + $scope.externalLdap.error.credentials = true; + } else if (error.statusCode === 400 && error.message === 'invalid usernameField') { + $scope.externalLdap.error.usernameField = true; + } else { + console.error('Failed to set external LDAP config:', error); + $scope.externalLdap.error.generic = error.message; + } + } else { + $('#externalLdapModal').modal('hide'); + + loadExternalLdapConfig(); + } + }); + } + }; + + function getUsers(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 refreshUsers(showBusy) { // loads users on current page only + if (showBusy) $scope.userRefreshBusy = true; + + getUsers(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 = { }; + for (var i = 0; i < result.length; i++) { + $scope.groupsById[result[i].id] = result[i]; + } + + if (callback) callback(); + }); + } + + function refresh() { + refreshGroups(function (error) { + if (error) return console.error('Unable to get group listing.', error); + refreshUsers(true); + }); + } + + function loadExternalLdapConfig() { + Client.getExternalLdapConfig(function (error, result) { + if (error) return console.error('Unable to get external ldap config.', error); + + $scope.externalLdap.currentConfig = result; + $scope.externalLdap.checkStatus(); + }); + } + + $scope.showNextPage = function () { + $scope.currentPage++; + refreshUsers(); + }; + + $scope.showPrevPage = function () { + if ($scope.currentPage > 1) $scope.currentPage--; + else $scope.currentPage = 1; + refreshUsers(); + }; + + $scope.updateFilter = function () { + refreshUsers(); + }; + + 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]; + } + }); + } + + function getAllDomains() { + Client.getDomains(function (error, domains) { + if (error) return console.error('Unable to get domains:', error); + + $scope.domains = domains; + }); + } + + Client.onReady(function () { + refresh(); + if ($scope.user.isAtLeastAdmin) loadExternalLdapConfig(); + if ($scope.user.isAtLeastAdmin) $scope.profileConfig.refresh(); + if ($scope.user.isAtLeastAdmin) $scope.userDirectoryConfig.refresh(); + if ($scope.user.isAtLeastAdmin) getAllDomains(); + 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); + }); + + new Clipboard('#userDirectoryUrlClipboardButton').on('success', function(e) { + $('#userDirectoryUrlClipboardButton').tooltip({ + title: 'Copied!', + trigger: 'manual' + }).tooltip('show'); + + $timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000); + + e.clearSelection(); + }).on('error', function(/*e*/) { + $('#userDirectoryUrlClipboardButton').tooltip({ + title: 'Press Ctrl+C to copy', + trigger: 'manual' + }).tooltip('show'); + + $timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000); + }); + + $('.modal-backdrop').remove(); +}]); diff --git a/dashboard/src/views/users.html b/dashboard/src/views/users.html index c31c80849..ab878435b 100644 --- a/dashboard/src/views/users.html +++ b/dashboard/src/views/users.html @@ -447,107 +447,6 @@ - - -
@@ -727,165 +626,6 @@
-
-

{{ 'users.externalLdap.title' | tr }}

-
- -
-
-
{{ 'users.externalLdap.description' | tr }}
-
- -
- -
-
- {{ 'users.externalLdap.subscriptionRequired' | tr }} {{ 'users.externalLdap.subscriptionRequiredAction' | tr }} -
-
- -
- -
-
- {{ 'users.externalLdap.noopInfo' | tr }} -
-
- -
-
- {{ 'users.externalLdap.provider' | tr }} -
-
- {{ externalLdap.currentConfig.provider }} -
-
- -
-
- {{ 'users.externalLdap.server' | tr }} -
-
- {{ externalLdap.currentConfig.url }} -
-
- -
-
- {{ 'users.externalLdap.acceptSelfSignedCert' | tr }} -
-
- {{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }} -
-
- -
-
- {{ 'users.externalLdap.baseDn' | tr }} -
-
- {{ externalLdap.currentConfig.baseDn }} -
-
- -
-
- {{ 'users.externalLdap.filter' | tr }} -
-
- {{ externalLdap.currentConfig.filter }} -
-
- -
-
- {{ 'users.externalLdap.usernameField' | tr }} -
-
- {{ externalLdap.currentConfig.usernameField || 'uid' }} -
-
- -
-
- {{ 'users.externalLdap.syncGroups' | tr }} -
-
- {{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }} -
-
- -
-
- {{ 'users.externalLdap.groupBaseDn' | tr }} -
-
- {{ externalLdap.currentConfig.groupBaseDn }} -
-
- -
-
- {{ 'users.externalLdap.groupFilter' | tr }} -
-
- {{ externalLdap.currentConfig.groupFilter }} -
-
- -
-
- {{ 'users.externalLdap.groupnameField' | tr }} -
-
- {{ externalLdap.currentConfig.groupnameField }} -
-
- -
-
- {{ 'users.externalLdap.auth' | tr }} -
-
- {{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }} -
-
- -
-
- {{ 'users.externalLdap.autocreateUsersOnLogin' | tr }} -
-
- {{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }} -
-
- -
-
-
-
-
-
-
-
- -
-
-

{{ externalLdap.message }}

-

-

{{ externalLdap.errorMessage }}
-

-
- -
- - - {{ 'users.externalLdap.showLogsAction' | tr }} -
-
-
-
-

{{ 'users.exposedLdap.title' | tr }}

diff --git a/dashboard/src/views/users.js b/dashboard/src/views/users.js index ff83b5b5f..968efa0ec 100644 --- a/dashboard/src/views/users.js +++ b/dashboard/src/views/users.js @@ -19,11 +19,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio { name: 'Disabled', value: 'noop' } ]; - $translate(['users.externalLdap.providerOther', 'users.externalLdap.providerDisabled']).then(function (tr) { - if (tr['users.externalLdap.providerOther']) $scope.ldapProvider.find(function (p) { return p.value === 'other'; }).name = tr['users.externalLdap.providerOther']; - if (tr['users.externalLdap.providerDisabled']) $scope.ldapProvider.find(function (p) { return p.value === 'noop'; }).name = tr['users.externalLdap.providerDisabled']; - }); - $scope.ready = false; $scope.users = []; // users of current page $scope.allUsersById = []; @@ -933,175 +928,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio } }; - $scope.externalLdap = { - busy: false, - percent: 0, - message: '', - errorMessage: '', - error: {}, - taskId: 0, - - syncBusy: false, - - // fields - provider: 'noop', - autoCreate: false, - url: '', - acceptSelfSignedCerts: false, - baseDn: '', - filter: '', - groupBaseDn: '', - bindDn: '', - bindPassword: '', - usernameField: '', - - currentConfig: {}, - - checkStatus: function () { - Client.getLatestTaskByType('syncExternalLdap', function (error, task) { - if (error) return console.error(error); - - if (!task) return; - - $scope.externalLdap.taskId = task.id; - $scope.externalLdap.updateStatus(); - }); - }, - - sync: function () { - $scope.externalLdap.syncBusy = true; - - Client.startExternalLdapSync(function (error, taskId) { - if (error) { - $scope.externalLdap.syncBusy = false; - console.error('Unable to start ldap syncer task.', error); - return; - } - - $scope.externalLdap.taskId = taskId; - $scope.externalLdap.updateStatus(); - }); - }, - - updateStatus: function () { - Client.getTask($scope.externalLdap.taskId, function (error, data) { - if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000); - - if (!data.active) { - $scope.externalLdap.syncBusy = false; - $scope.externalLdap.message = ''; - $scope.externalLdap.percent = 100; // indicates that 'result' is valid - $scope.externalLdap.errorMessage = data.success ? '' : data.error.message; - - refreshGroups(); - refreshUsers(); - - return; - } - - $scope.externalLdap.syncBusy = true; - $scope.externalLdap.percent = data.percent; - $scope.externalLdap.message = data.message; - window.setTimeout($scope.externalLdap.updateStatus, 3000); - }); - }, - - show: function () { - $scope.externalLdap.busy = false; - $scope.externalLdap.error = {}; - - $scope.externalLdap.provider = $scope.externalLdap.currentConfig.provider; - $scope.externalLdap.url = $scope.externalLdap.currentConfig.url; - $scope.externalLdap.acceptSelfSignedCerts = $scope.externalLdap.currentConfig.acceptSelfSignedCerts; - $scope.externalLdap.baseDn = $scope.externalLdap.currentConfig.baseDn; - $scope.externalLdap.filter = $scope.externalLdap.currentConfig.filter; - $scope.externalLdap.syncGroups = $scope.externalLdap.currentConfig.syncGroups; - $scope.externalLdap.groupBaseDn = $scope.externalLdap.currentConfig.groupBaseDn; - $scope.externalLdap.groupFilter = $scope.externalLdap.currentConfig.groupFilter; - $scope.externalLdap.groupnameField = $scope.externalLdap.currentConfig.groupnameField; - $scope.externalLdap.bindDn = $scope.externalLdap.currentConfig.bindDn; - $scope.externalLdap.bindPassword = $scope.externalLdap.currentConfig.bindPassword; - $scope.externalLdap.usernameField = $scope.externalLdap.currentConfig.usernameField; - $scope.externalLdap.autoCreate = $scope.externalLdap.currentConfig.autoCreate; - - $('#externalLdapModal').modal('show'); - }, - - submit: function () { - $scope.externalLdap.busy = true; - $scope.externalLdap.error = {}; - - var config = { - provider: $scope.externalLdap.provider - }; - - if ($scope.externalLdap.provider === 'cloudron') { - config.url = $scope.externalLdap.url; - config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts; - config.autoCreate = $scope.externalLdap.autoCreate; - config.syncGroups = $scope.externalLdap.syncGroups; - config.bindPassword = $scope.externalLdap.bindPassword; - - // those values are known and thus overwritten - config.baseDn = 'ou=users,dc=cloudron'; - config.filter = '(objectClass=inetOrgPerson)'; - config.usernameField = 'username'; - config.groupBaseDn = 'ou=groups,dc=cloudron'; - config.groupFilter = '(objectClass=group)'; - config.groupnameField = 'cn'; - config.bindDn = 'cn=admin,ou=system,dc=cloudron'; - } else if ($scope.externalLdap.provider !== 'noop') { - config.url = $scope.externalLdap.url; - config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts; - config.baseDn = $scope.externalLdap.baseDn; - config.filter = $scope.externalLdap.filter; - config.usernameField = $scope.externalLdap.usernameField; - config.syncGroups = $scope.externalLdap.syncGroups; - config.groupBaseDn = $scope.externalLdap.groupBaseDn; - config.groupFilter = $scope.externalLdap.groupFilter; - config.groupnameField = $scope.externalLdap.groupnameField; - config.autoCreate = $scope.externalLdap.autoCreate; - - if ($scope.externalLdap.bindDn) { - config.bindDn = $scope.externalLdap.bindDn; - config.bindPassword = $scope.externalLdap.bindPassword; - } - } - - Client.setExternalLdapConfig(config, function (error) { - $scope.externalLdap.busy = false; - - if (error) { - if (error.statusCode === 424) { - if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') $scope.externalLdap.error.acceptSelfSignedCerts = true; - else $scope.externalLdap.error.url = true; - } else if (error.statusCode === 400 && error.message === 'invalid baseDn') { - $scope.externalLdap.error.baseDn = true; - } else if (error.statusCode === 400 && error.message === 'invalid filter') { - $scope.externalLdap.error.filter = true; - } else if (error.statusCode === 400 && error.message === 'invalid groupBaseDn') { - $scope.externalLdap.error.groupBaseDn = true; - } else if (error.statusCode === 400 && error.message === 'invalid groupFilter') { - $scope.externalLdap.error.groupFilter = true; - } else if (error.statusCode === 400 && error.message === 'invalid groupnameField') { - $scope.externalLdap.error.groupnameField = true; - } else if (error.statusCode === 400 && error.message === 'invalid bind credentials') { - $scope.externalLdap.error.credentials = true; - } else if (error.statusCode === 400 && error.message === 'invalid usernameField') { - $scope.externalLdap.error.usernameField = true; - } else { - console.error('Failed to set external LDAP config:', error); - $scope.externalLdap.error.generic = error.message; - } - } else { - $('#externalLdapModal').modal('hide'); - - loadExternalLdapConfig(); - } - }); - } - }; - function getUsers(callback) { var users = []; @@ -1159,15 +985,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio }); } - function loadExternalLdapConfig() { - Client.getExternalLdapConfig(function (error, result) { - if (error) return console.error('Unable to get external ldap config.', error); - - $scope.externalLdap.currentConfig = result; - $scope.externalLdap.checkStatus(); - }); - } - $scope.showNextPage = function () { $scope.currentPage++; refreshUsers(); @@ -1206,7 +1023,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio Client.onReady(function () { refresh(); - if ($scope.user.isAtLeastAdmin) loadExternalLdapConfig(); if ($scope.user.isAtLeastAdmin) $scope.profileConfig.refresh(); if ($scope.user.isAtLeastAdmin) $scope.userDirectoryConfig.refresh(); if ($scope.user.isAtLeastAdmin) getAllDomains();