diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 9abe37227..7b73a8063 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -53,6 +53,20 @@ _default_origins = [ # Merge environment and default origins, avoiding duplicates CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(_csrf_origins_list + _default_origins)) +# Optional file: one trusted origin per line (e.g. https://203.0.113.1:2087) for IP:port panel access. +# Create /etc/cyberpanel/csrf_trusted_origins on the server if JSON POSTs get 403 CSRF when using HTTPS by IP. +_csrf_trusted_origins_file = '/etc/cyberpanel/csrf_trusted_origins' +if os.path.isfile(_csrf_trusted_origins_file): + try: + with open(_csrf_trusted_origins_file, 'r', encoding='utf-8', errors='replace') as _csrf_f: + for _csrf_line in _csrf_f: + _csrf_line = _csrf_line.strip() + if _csrf_line and not _csrf_line.startswith('#'): + if _csrf_line not in CSRF_TRUSTED_ORIGINS: + CSRF_TRUSTED_ORIGINS.append(_csrf_line) + except OSError: + pass + # Application definition INSTALLED_APPS = [ diff --git a/public/static/userManagment/userManagment.js b/public/static/userManagment/userManagment.js index 0f0b677af..8a0b21649 100644 --- a/public/static/userManagment/userManagment.js +++ b/public/static/userManagment/userManagment.js @@ -2,10 +2,61 @@ * Created by usman on 8/5/17. */ +/* Safe notification - use PNotify if available, else fallback to alert */ +function safePNotify(opts) { + if (typeof PNotify !== 'undefined') { + new PNotify(opts); + } else { + var msg = (opts.title || '') + (opts.text ? ': ' + opts.text : ''); + alert(msg || JSON.stringify(opts)); + } +} /* Java script code to create account */ app.controller('createUserCtr', function ($scope, $http) { + // Home directory functionality + $scope.homeDirectories = []; + $scope.selectedHomeDirectory = ''; + $scope.selectedHomeDirectoryInfo = null; + + // Load home directories on page load + $scope.loadHomeDirectories = function() { + var url = '/users/getUserHomeDirectories'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + $http.post(url, {}, config) + .then(function(response) { + if (response.data && response.data.status === 1) { + $scope.homeDirectories = response.data.directories || []; + } else { + console.error('Error loading home directories:', response.data); + $scope.homeDirectories = []; + } + }) + .catch(function(error) { + console.error('Error loading home directories:', error); + $scope.homeDirectories = []; + }); + }; + + // Update home directory info when selection changes + $scope.updateHomeDirectoryInfo = function() { + if ($scope.selectedHomeDirectory) { + $scope.selectedHomeDirectoryInfo = $scope.homeDirectories.find(function(dir) { + return dir.id == $scope.selectedHomeDirectory; + }); + } else { + $scope.selectedHomeDirectoryInfo = null; + } + }; + + // Initialize home directories + $scope.loadHomeDirectories(); + $scope.acctsLimit = true; $scope.webLimits = true; $scope.userCreated = true; @@ -23,15 +74,19 @@ app.controller('createUserCtr', function ($scope, $http) { $scope.userCreationLoading = false; $scope.combinedLength = true; - - var firstName = $scope.firstName; - var lastName = $scope.lastName; + var firstName = $scope.firstName || ''; + var lastName = $scope.lastName || ''; var email = $scope.email; var selectedACL = $scope.selectedACL; var websitesLimits = $scope.websitesLimits; var userName = $scope.userName; var password = $scope.password; + if (firstName.length + lastName.length > 20) { + $scope.combinedLength = false; + $scope.userCreationLoading = true; + return; + } var url = "/users/submitUserCreation"; @@ -43,12 +98,14 @@ app.controller('createUserCtr', function ($scope, $http) { websitesLimit: websitesLimits, userName: userName, password: password, - securityLevel: $scope.securityLevel + securityLevel: $scope.securityLevel, + selectedHomeDirectory: $scope.selectedHomeDirectory || '' }; var config = { headers: { - 'X-CSRFToken': getCookie('csrftoken') + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': 'application/json' } }; @@ -59,42 +116,31 @@ app.controller('createUserCtr', function ($scope, $http) { if (response.data.createStatus == 1) { - - $scope.userCreated = false; - $scope.userCreationFailed = false; // hide error on success + $scope.userCreated = false; // show success + $scope.userCreationFailed = false; // hide error $scope.couldNotConnect = true; $scope.userCreationLoading = true; - $scope.userName = userName; - - } else { - $scope.acctsLimit = false; $scope.webLimits = false; $scope.userCreated = true; - $scope.userCreationFailed = true; // show error on failure + $scope.userCreationFailed = true; // true = show error alert $scope.couldNotConnect = true; $scope.userCreationLoading = true; - $scope.errorMessage = (response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; - - } } function cantLoadInitialDatas(response) { - $scope.acctsLimit = false; $scope.webLimits = false; $scope.userCreated = true; - $scope.userCreationFailed = false; // show "Could not connect" instead - $scope.couldNotConnect = false; + $scope.userCreationFailed = false; // hide server error, show connection error instead + $scope.couldNotConnect = false; // show "Could not connect" message $scope.userCreationLoading = true; - - } @@ -125,14 +171,28 @@ app.controller('createUserCtr', function ($scope, $http) { /* Java script code to modify user account */ -app.controller('modifyUser', function ($scope, $http) { +app.controller('modifyUser', function ($scope, $http, $timeout) { - var qrCode = window.qr = new QRious({ - element: document.getElementById('qr'), + var qrEl = document.getElementById('qr'); + var qrCode = window.qr = (qrEl && typeof QRious !== 'undefined') ? new QRious({ + element: qrEl, size: 200, value: 'QRious' - }); + }) : null; + if (!qrCode && qrEl) { + try { window.qr = new QRious({ element: qrEl, size: 200, value: 'QRious' }); } catch (e) { /* ignore */ } + } + $scope.userSearch = ''; + /* Prefer global set by inline script (before Angular); fallback to script tag */ + var list = (typeof window.__CP_ACCT_NAMES !== 'undefined' && Array.isArray(window.__CP_ACCT_NAMES)) + ? window.__CP_ACCT_NAMES + : (function() { + var el = document.getElementById('acctNamesData'); + if (!el || !el.textContent) return []; + try { return JSON.parse(el.textContent); } catch (e) { return []; } + })(); + $scope.acctNamesList = Array.isArray(list) ? list : []; $scope.userModificationLoading = true; $scope.acctDetailsFetched = true; @@ -153,6 +213,223 @@ app.controller('modifyUser', function ($scope, $http) { $scope.qrHidden = true; } }; + + $scope.copySecretKey = function() { + if ($scope.secretKey) { + // Create a temporary textarea element + var tempTextarea = document.createElement('textarea'); + tempTextarea.value = $scope.secretKey; + tempTextarea.style.position = 'fixed'; + tempTextarea.style.opacity = '0'; + document.body.appendChild(tempTextarea); + + // Select and copy the text + tempTextarea.select(); + tempTextarea.setSelectionRange(0, 99999); // For mobile devices + + try { + document.execCommand('copy'); + // Show success feedback (you can add a toast notification here if available) + alert('Secret key copied to clipboard!'); + } catch (err) { + alert('Failed to copy secret key. Please copy it manually.'); + } + + // Remove the temporary element + document.body.removeChild(tempTextarea); + } + }; + + $scope.regenerateSecret = function() { + if (!$scope.accountUsername) { + alert('Please select a user first.'); + return; + } + + if (!confirm('Are you sure you want to regenerate the 2FA secret? This will generate a new secret key and you will need to update your authenticator app.')) { + return; + } + + var url = "/users/regenerateTwoFASecret"; + var data = { + accountUsername: $scope.accountUsername + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + if (response.data.status === 1) { + // Update the secret key and formatted version + $scope.secretKey = response.data.secretKey; + $scope.formattedSecretKey = response.data.secretKey.match(/.{1,4}/g).join(' '); + + // Update the QR code with new provisioning URI + if (qrCode) qrCode.set({ value: response.data.otpauth }); + + // Show success message + alert('2FA secret has been successfully regenerated! Please update your authenticator app with the new QR code or secret key.'); + } else { + alert('Error regenerating 2FA secret: ' + response.data.error_message); + } + }, function(error) { + console.error('Error regenerating 2FA secret:', error); + alert('Failed to regenerate 2FA secret. Please try again.'); + }); + }; + + // WebAuthn Functions + $scope.loadWebAuthnData = function() { + if (!$scope.accountUsername) return; + $scope.webauthnDataLoaded = false; + var url = '/webauthn/credentials/' + $scope.accountUsername + '/'; + $http.get(url).then(function(response) { + if (response.data.success) { + $scope.webauthnCredentials = response.data.credentials || []; + $scope.webauthnEnabled = response.data.settings.enabled; + $scope.webauthnRequirePasskey = response.data.settings.require_passkey; + $scope.webauthnAllowMultiple = response.data.settings.allow_multiple_credentials; + $scope.webauthnMaxCredentials = response.data.settings.max_credentials; + $scope.canAddCredential = !!response.data.settings.can_add_credential; + $scope.webauthnDataLoaded = true; + } else { + $scope.canAddCredential = true; + } + }, function(error) { + console.error('Error loading WebAuthn data:', error); + $scope.canAddCredential = true; + $scope.webauthnCredentials = []; + }); + }; + + $scope.toggleWebAuthn = function() { + if ($scope.webauthnEnabled) { + $scope.loadWebAuthnData(); + } else { + $scope.webauthnCredentials = []; + $scope.canAddCredential = true; + $scope.webauthnDataLoaded = false; + } + }; + + /* Inline passkey UX like diabetes.newstargeted.com/profile?tab=2fa: name input + button + message area, no modal */ + $scope.newPasskeyName = ''; + $scope.webauthnMessage = ''; + $scope.webauthnMessageError = false; + $scope.registerPasskeyLoading = false; + + $scope.registerNewPasskey = function() { + if (!$scope.accountUsername) { + $scope.webauthnMessage = 'Please select a user account first.'; + $scope.webauthnMessageError = true; + return; + } + if (typeof window.cyberPanelWebAuthn === 'undefined') { + $scope.webauthnMessage = 'WebAuthn script not loaded. Refresh the page (Ctrl+F5) and try again.'; + $scope.webauthnMessageError = true; + return; + } + if (typeof window.cyberPanelWebAuthn.isSupported !== 'function' || !window.cyberPanelWebAuthn.isSupported()) { + $scope.webauthnMessage = 'WebAuthn is not supported in this browser.'; + $scope.webauthnMessageError = true; + return; + } + var name = ($scope.newPasskeyName || '').trim() || 'Security key'; + $scope.webauthnMessage = ''; + $scope.webauthnMessageError = false; + $scope.registerPasskeyLoading = true; + var username = $scope.accountUsername; + window.cyberPanelWebAuthn.registerPasskey(username, name, { silent: true }) + .then(function(response) { + if (response && response.success) { + $scope.webauthnMessage = 'Passkey registered successfully.'; + $scope.webauthnMessageError = false; + $scope.newPasskeyName = ''; + $timeout(function() { $scope.loadWebAuthnData(); }, 0); + } else { + $scope.webauthnMessage = (response && response.error) ? response.error : 'Registration failed.'; + $scope.webauthnMessageError = true; + } + }) + .catch(function(error) { + var msg = (error && error.message) ? error.message : 'Passkey registration failed.'; + if (error && error.name === 'NotAllowedError') msg = 'Registration was cancelled or timed out.'; + $scope.webauthnMessage = msg; + $scope.webauthnMessageError = true; + console.error('Error registering passkey:', error); + }) + .finally(function() { + $scope.registerPasskeyLoading = false; + if (!$scope.$$phase && !$scope.$root.$$phase) { try { $scope.$apply(); } catch (e) { /* already applied */ } } + }); + }; + + $scope.deleteCredential = function(credentialId) { + if (!confirm('Are you sure you want to delete this passkey?')) return; + + if (!window.cyberPanelWebAuthn) { + alert('WebAuthn is not supported in this browser'); + return; + } + + window.cyberPanelWebAuthn.deleteCredential($scope.accountUsername, credentialId) + .then(function(response) { + if (response.success) { + $scope.loadWebAuthnData(); + $scope.$apply(); + } + }) + .catch(function(error) { + console.error('Error deleting credential:', error); + }); + }; + + $scope.updateCredentialName = function(credentialId, newName) { + if (!window.cyberPanelWebAuthn) return; + + window.cyberPanelWebAuthn.updateCredentialName($scope.accountUsername, credentialId, newName) + .then(function(response) { + if (response.success) { + $scope.loadWebAuthnData(); + $scope.$apply(); + } + }) + .catch(function(error) { + console.error('Error updating credential name:', error); + }); + }; + + $scope.refreshCredentials = function() { + $scope.loadWebAuthnData(); + }; + + $scope.saveWebAuthnSettings = function() { + if (!window.cyberPanelWebAuthn) { + alert('WebAuthn is not supported in this browser'); + return; + } + + var settings = { + enabled: $scope.webauthnEnabled, + require_passkey: $scope.webauthnRequirePasskey, + allow_multiple_credentials: $scope.webauthnAllowMultiple, + max_credentials: $scope.webauthnMaxCredentials, + timeout_seconds: $scope.webauthnTimeout + }; + + window.cyberPanelWebAuthn.updateSettings($scope.accountUsername, settings) + .then(function(response) { + if (response.success) { + $scope.loadWebAuthnData(); + $scope.$apply(); + } + }) + .catch(function(error) { + console.error('Error updating WebAuthn settings:', error); + }); + }; $scope.fetchUserDetails = function () { @@ -191,11 +468,26 @@ app.controller('modifyUser', function ($scope, $http) { $scope.securityLevel = userDetails.securityLevel; $scope.currentSecurityLevel = userDetails.securityLevel; $scope.twofa = Boolean(userDetails.twofa); + + // Format secret key with spaces for better readability + if (userDetails.secretKey) { + $scope.secretKey = userDetails.secretKey; + $scope.formattedSecretKey = userDetails.secretKey.match(/.{1,4}/g).join(' '); + } + + // Initialize WebAuthn settings + $scope.webauthnEnabled = false; + $scope.webauthnRequirePasskey = false; + $scope.webauthnAllowMultiple = true; + $scope.webauthnMaxCredentials = 10; + $scope.webauthnTimeout = 60; + $scope.webauthnCredentials = []; + $scope.canAddCredential = true; + $scope.webauthnDataLoaded = false; + // Load WebAuthn settings and credentials + $scope.loadWebAuthnData(); - qrCode.set({ - value: userDetails.otpauth - }); - + if (qrCode) qrCode.set({ value: userDetails.otpauth }); $scope.userModificationLoading = true; $scope.acctDetailsFetched = false; @@ -214,7 +506,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModified = true; $scope.canotModifyUser = false; // hide modify error (only fetch failed) $scope.couldNotConnect = true; - $scope.canotFetchDetails = true; // show fetch error + $scope.canotFetchDetails = true; // show fetch error on failure $scope.detailsFetched = false; @@ -249,7 +541,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = false; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = false; // hide error until we know result + $scope.canotModifyUser = false; // hide modify error until we know result $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -273,18 +565,28 @@ app.controller('modifyUser', function ($scope, $http) { firstName: firstName, lastName: lastName, email: email, - passwordByPass: password, securityLevel: $scope.securityLevel, twofa: $scope.twofa }; + // Only include password if it's provided and not empty + if (password && password.trim()) { + data.passwordByPass = password; + } + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + $http.post(url, data, config).then(function(response) { + ListInitialDatas(response); + // Save WebAuthn settings after successful user modification (only if WebAuthn script loaded) + if (response.data.saveStatus == 1 && window.cyberPanelWebAuthn) { + $scope.saveWebAuthnSettings(); + } + }, cantLoadInitialDatas); function ListInitialDatas(response) { @@ -297,7 +599,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModified = false; $scope.canotModifyUser = false; // hide modify error on success $scope.couldNotConnect = true; - $scope.canotFetchDetails = true; + $scope.canotFetchDetails = false; // hide "Cannot fetch details" on save success $scope.detailsFetched = true; $scope.userAccountsLimit = true; $scope.accountTypeView = true; @@ -312,7 +614,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = true; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = true; // show modify error + $scope.canotModifyUser = true; // show modify error on failure $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -335,7 +637,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.couldNotConnect = false; $scope.canotFetchDetails = true; $scope.detailsFetched = true; - + $scope.errorMessage = (response && response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; } @@ -461,6 +763,10 @@ app.controller('deleteUser', function ($scope, $http) { app.controller('createACLCTRL', function ($scope, $http) { + $scope.aclCreated = true; + $scope.aclCreationFailed = false; // false = don't show error alert on load + $scope.couldNotConnect = true; + $scope.aclLoading = true; $scope.makeAdmin = false; @@ -629,14 +935,14 @@ app.controller('createACLCTRL', function ($scope, $http) { $scope.aclLoading = true; if (response.data.status === 1) { - new PNotify({ + safePNotify({ title: 'Success!', text: 'ACL Successfully created.', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -651,7 +957,7 @@ app.controller('createACLCTRL', function ($scope, $http) { $scope.aclLoading = false; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -781,7 +1087,7 @@ app.controller('createACLCTRL', function ($scope, $http) { // Email Management $scope.createEmail = true; - $scope.listEmails = True; + $scope.listEmails = true; $scope.deleteEmail = true; $scope.emailForwarding = true; $scope.changeEmailPassword = true; @@ -850,14 +1156,14 @@ app.controller('deleteACTCTRL', function ($scope, $http) { $scope.aclLoading = true; if (response.data.status === 1) { - new PNotify({ + safePNotify({ title: 'Success!', text: 'ACL Successfully deleted.', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -869,7 +1175,7 @@ app.controller('deleteACTCTRL', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.aclLoading = true; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -913,7 +1219,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) { if (response.data.status === 1) { - new PNotify({ + safePNotify({ title: 'Success!', text: 'Current settings successfully fetched', type: 'success' @@ -993,7 +1299,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) { $scope.mailServerSSL = Boolean(response.data.mailServerSSL); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -1006,7 +1312,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) { $scope.aclLoading = false; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1108,14 +1414,14 @@ app.controller('modifyACLCtrl', function ($scope, $http) { $scope.aclLoading = true; if (response.data.status === 1) { - new PNotify({ + safePNotify({ title: 'Success!', text: 'ACL Successfully modified.', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -1130,7 +1436,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) { $scope.aclLoading = false; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1260,7 +1566,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) { // Email Management $scope.createEmail = true; - $scope.listEmails = True; + $scope.listEmails = true; $scope.deleteEmail = true; $scope.emailForwarding = true; $scope.changeEmailPassword = true; @@ -1322,14 +1628,14 @@ app.controller('changeUserACLCTRL', function ($scope, $http) { $scope.aclLoading = true; if (response.data.status === 1) { - new PNotify({ + safePNotify({ title: 'Success!', text: 'ACL Successfully changed.', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -1341,7 +1647,7 @@ app.controller('changeUserACLCTRL', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.aclLoading = true; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1384,14 +1690,14 @@ app.controller('resellerCenterCTRL', function ($scope, $http) { $scope.aclLoading = true; if (response.data.status === 1) { - new PNotify({ + safePNotify({ title: 'Success!', text: 'Changes successfully applied!', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -1403,7 +1709,7 @@ app.controller('resellerCenterCTRL', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.aclLoading = true; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1453,14 +1759,14 @@ app.controller('apiAccessCTRL', function ($scope, $http) { if (response.data.status === 1) { $scope.apiAccessDropDown = true; - new PNotify({ + safePNotify({ title: 'Success!', text: 'Changes successfully applied!', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.error_message, type: 'error' @@ -1472,7 +1778,7 @@ app.controller('apiAccessCTRL', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.cyberpanelLoading = true; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1486,6 +1792,147 @@ app.controller('apiAccessCTRL', function ($scope, $http) { }); /* Java script code for api access */ +/* Java script code for api users list */ +app.controller('apiUsersCTRL', function ($scope, $http) { + $scope.apiUsers = []; + $scope.filteredUsers = []; + $scope.searchQuery = ''; + $scope.apiUsersLoading = true; + + $scope.loadAPIUsers = function() { + $scope.apiUsersLoading = false; + + var url = "/users/fetchAPIUsers"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.get(url, config).then(loadAPIUsersSuccess, loadAPIUsersError); + }; + + function loadAPIUsersSuccess(response) { + $scope.apiUsersLoading = true; + + if (response.data.status === 1) { + $scope.apiUsers = response.data.users; + $scope.filteredUsers = response.data.users; + + safePNotify({ + title: 'Success!', + text: 'API users loaded successfully', + type: 'success' + }); + } else { + safePNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + } + + function loadAPIUsersError(response) { + $scope.apiUsersLoading = true; + safePNotify({ + title: 'Error!', + text: 'Could not load API users. Please refresh the page.', + type: 'error' + }); + } + + $scope.searchUsers = function() { + if (!$scope.searchQuery || $scope.searchQuery.trim() === '') { + $scope.filteredUsers = $scope.apiUsers; + return; + } + + var query = $scope.searchQuery.toLowerCase(); + $scope.filteredUsers = $scope.apiUsers.filter(function(user) { + return user.userName.toLowerCase().includes(query) || + user.firstName.toLowerCase().includes(query) || + user.lastName.toLowerCase().includes(query) || + user.email.toLowerCase().includes(query) || + user.aclName.toLowerCase().includes(query); + }); + }; + + $scope.clearSearch = function() { + $scope.searchQuery = ''; + $scope.filteredUsers = $scope.apiUsers; + }; + + $scope.viewUserDetails = function(user) { + safePNotify({ + title: 'User Details', + text: 'Username: ' + user.userName + '
' + + 'Full Name: ' + user.firstName + ' ' + user.lastName + '
' + + 'Email: ' + user.email + '
' + + 'ACL: ' + user.aclName + '
' + + 'Token Status: ' + user.tokenStatus + '
' + + 'State: ' + user.state, + type: 'info', + styling: 'bootstrap3', + delay: 10000 + }); + }; + + $scope.disableAPI = function(user) { + if (confirm('Are you sure you want to disable API access for ' + user.userName + '?')) { + $scope.apiUsersLoading = false; + + var url = "/users/saveChangesAPIAccess"; + var data = { + accountUsername: user.userName, + access: 'Disable' + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(disableAPISuccess, disableAPIError); + } + }; + + function disableAPISuccess(response) { + $scope.apiUsersLoading = true; + + if (response.data.status === 1) { + // Remove user from the list + $scope.apiUsers = $scope.apiUsers.filter(function(u) { + return u.userName !== response.data.accountUsername; + }); + $scope.filteredUsers = $scope.apiUsers; + + safePNotify({ + title: 'Success!', + text: 'API access disabled for ' + response.data.accountUsername, + type: 'success' + }); + } else { + safePNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + } + + function disableAPIError(response) { + $scope.apiUsersLoading = true; + safePNotify({ + title: 'Error!', + text: 'Could not disable API access. Please try again.', + type: 'error' + }); + } + + // Load API users when controller initializes + $scope.loadAPIUsers(); +}); /* Java script code to list table users */ @@ -1556,14 +2003,14 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.records = JSON.parse(response.data.data); - new PNotify({ + safePNotify({ title: 'Success!', text: 'Users successfully fetched!', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.error_message, type: 'error' @@ -1574,7 +2021,7 @@ app.controller('listTableUsers', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.cyberpanelLoading = true; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1614,7 +2061,7 @@ app.controller('listTableUsers', function ($scope, $http) { if (response.data.deleteStatus === 1) { $scope.populateCurrentRecords(); hideModalById('deleteModal'); - new PNotify({ + safePNotify({ title: 'Success!', text: 'Users successfully deleted!', type: 'success' @@ -1622,7 +2069,7 @@ app.controller('listTableUsers', function ($scope, $http) { } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.error_message, type: 'error' @@ -1636,7 +2083,7 @@ app.controller('listTableUsers', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.cyberpanelLoading = false; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1649,10 +2096,8 @@ app.controller('listTableUsers', function ($scope, $http) { }; $scope.editInitial = function (name) { - $scope.name = name; showModalById('editModal'); - }; $scope.saveResellerChanges = function () { @@ -1680,14 +2125,14 @@ app.controller('listTableUsers', function ($scope, $http) { if (response.data.status === 1) { $scope.populateCurrentRecords(); hideModalById('editModal'); - new PNotify({ + safePNotify({ title: 'Success!', text: 'Changes successfully applied!', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -1698,7 +2143,7 @@ app.controller('listTableUsers', function ($scope, $http) { } function cantLoadInitialDatas(response) { - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1734,14 +2179,14 @@ app.controller('listTableUsers', function ($scope, $http) { if (response.data.status === 1) { $scope.populateCurrentRecords(); hideModalById('editModal'); - new PNotify({ + safePNotify({ title: 'Success!', text: 'ACL Successfully changed.', type: 'success' }); } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.errorMessage, type: 'error' @@ -1753,7 +2198,7 @@ app.controller('listTableUsers', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.aclLoading = true; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' @@ -1764,6 +2209,7 @@ app.controller('listTableUsers', function ($scope, $http) { }; $scope.controlUserState = function (userName, state) { + $scope.cyberpanelLoading = false; var url = "/users/controlUserState"; @@ -1786,7 +2232,7 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.cyberpanelLoading = true; if (response.data.status === 1) { $scope.populateCurrentRecords(); - new PNotify({ + safePNotify({ title: 'Success!', text: 'Action successfully started.', type: 'success' @@ -1794,7 +2240,7 @@ app.controller('listTableUsers', function ($scope, $http) { } else { - new PNotify({ + safePNotify({ title: 'Error!', text: response.data.error_message, type: 'error' @@ -1808,7 +2254,7 @@ app.controller('listTableUsers', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.cyberpanelLoading = false; - new PNotify({ + safePNotify({ title: 'Error!', text: 'Could not connect to server, please refresh this page.', type: 'error' diff --git a/static/userManagment/userManagment.js b/static/userManagment/userManagment.js index 071dcec34..8a0b21649 100644 --- a/static/userManagment/userManagment.js +++ b/static/userManagment/userManagment.js @@ -171,14 +171,28 @@ app.controller('createUserCtr', function ($scope, $http) { /* Java script code to modify user account */ -app.controller('modifyUser', function ($scope, $http) { +app.controller('modifyUser', function ($scope, $http, $timeout) { - var qrCode = window.qr = new QRious({ - element: document.getElementById('qr'), + var qrEl = document.getElementById('qr'); + var qrCode = window.qr = (qrEl && typeof QRious !== 'undefined') ? new QRious({ + element: qrEl, size: 200, value: 'QRious' - }); + }) : null; + if (!qrCode && qrEl) { + try { window.qr = new QRious({ element: qrEl, size: 200, value: 'QRious' }); } catch (e) { /* ignore */ } + } + $scope.userSearch = ''; + /* Prefer global set by inline script (before Angular); fallback to script tag */ + var list = (typeof window.__CP_ACCT_NAMES !== 'undefined' && Array.isArray(window.__CP_ACCT_NAMES)) + ? window.__CP_ACCT_NAMES + : (function() { + var el = document.getElementById('acctNamesData'); + if (!el || !el.textContent) return []; + try { return JSON.parse(el.textContent); } catch (e) { return []; } + })(); + $scope.acctNamesList = Array.isArray(list) ? list : []; $scope.userModificationLoading = true; $scope.acctDetailsFetched = true; @@ -253,9 +267,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.formattedSecretKey = response.data.secretKey.match(/.{1,4}/g).join(' '); // Update the QR code with new provisioning URI - qrCode.set({ - value: response.data.otpauth - }); + if (qrCode) qrCode.set({ value: response.data.otpauth }); // Show success message alert('2FA secret has been successfully regenerated! Please update your authenticator app with the new QR code or secret key.'); @@ -271,20 +283,24 @@ app.controller('modifyUser', function ($scope, $http) { // WebAuthn Functions $scope.loadWebAuthnData = function() { if (!$scope.accountUsername) return; - + $scope.webauthnDataLoaded = false; var url = '/webauthn/credentials/' + $scope.accountUsername + '/'; - $http.get(url).then(function(response) { if (response.data.success) { - $scope.webauthnCredentials = response.data.credentials; + $scope.webauthnCredentials = response.data.credentials || []; $scope.webauthnEnabled = response.data.settings.enabled; $scope.webauthnRequirePasskey = response.data.settings.require_passkey; $scope.webauthnAllowMultiple = response.data.settings.allow_multiple_credentials; $scope.webauthnMaxCredentials = response.data.settings.max_credentials; - $scope.canAddCredential = response.data.settings.can_add_credential; + $scope.canAddCredential = !!response.data.settings.can_add_credential; + $scope.webauthnDataLoaded = true; + } else { + $scope.canAddCredential = true; } }, function(error) { console.error('Error loading WebAuthn data:', error); + $scope.canAddCredential = true; + $scope.webauthnCredentials = []; }); }; @@ -294,27 +310,59 @@ app.controller('modifyUser', function ($scope, $http) { } else { $scope.webauthnCredentials = []; $scope.canAddCredential = true; + $scope.webauthnDataLoaded = false; } }; + /* Inline passkey UX like diabetes.newstargeted.com/profile?tab=2fa: name input + button + message area, no modal */ + $scope.newPasskeyName = ''; + $scope.webauthnMessage = ''; + $scope.webauthnMessageError = false; + $scope.registerPasskeyLoading = false; + $scope.registerNewPasskey = function() { - if (!window.cyberPanelWebAuthn) { - alert('WebAuthn is not supported in this browser'); + if (!$scope.accountUsername) { + $scope.webauthnMessage = 'Please select a user account first.'; + $scope.webauthnMessageError = true; return; } - - var credentialName = prompt('Enter a name for this passkey:', 'Passkey ' + new Date().toLocaleDateString()); - if (!credentialName) return; - - window.cyberPanelWebAuthn.registerPasskey($scope.accountUsername, credentialName) + if (typeof window.cyberPanelWebAuthn === 'undefined') { + $scope.webauthnMessage = 'WebAuthn script not loaded. Refresh the page (Ctrl+F5) and try again.'; + $scope.webauthnMessageError = true; + return; + } + if (typeof window.cyberPanelWebAuthn.isSupported !== 'function' || !window.cyberPanelWebAuthn.isSupported()) { + $scope.webauthnMessage = 'WebAuthn is not supported in this browser.'; + $scope.webauthnMessageError = true; + return; + } + var name = ($scope.newPasskeyName || '').trim() || 'Security key'; + $scope.webauthnMessage = ''; + $scope.webauthnMessageError = false; + $scope.registerPasskeyLoading = true; + var username = $scope.accountUsername; + window.cyberPanelWebAuthn.registerPasskey(username, name, { silent: true }) .then(function(response) { - if (response.success) { - $scope.loadWebAuthnData(); - $scope.$apply(); + if (response && response.success) { + $scope.webauthnMessage = 'Passkey registered successfully.'; + $scope.webauthnMessageError = false; + $scope.newPasskeyName = ''; + $timeout(function() { $scope.loadWebAuthnData(); }, 0); + } else { + $scope.webauthnMessage = (response && response.error) ? response.error : 'Registration failed.'; + $scope.webauthnMessageError = true; } }) .catch(function(error) { + var msg = (error && error.message) ? error.message : 'Passkey registration failed.'; + if (error && error.name === 'NotAllowedError') msg = 'Registration was cancelled or timed out.'; + $scope.webauthnMessage = msg; + $scope.webauthnMessageError = true; console.error('Error registering passkey:', error); + }) + .finally(function() { + $scope.registerPasskeyLoading = false; + if (!$scope.$$phase && !$scope.$root.$$phase) { try { $scope.$apply(); } catch (e) { /* already applied */ } } }); }; @@ -435,14 +483,11 @@ app.controller('modifyUser', function ($scope, $http) { $scope.webauthnTimeout = 60; $scope.webauthnCredentials = []; $scope.canAddCredential = true; - + $scope.webauthnDataLoaded = false; // Load WebAuthn settings and credentials $scope.loadWebAuthnData(); - qrCode.set({ - value: userDetails.otpauth - }); - + if (qrCode) qrCode.set({ value: userDetails.otpauth }); $scope.userModificationLoading = true; $scope.acctDetailsFetched = false; @@ -537,8 +582,8 @@ app.controller('modifyUser', function ($scope, $http) { $http.post(url, data, config).then(function(response) { ListInitialDatas(response); - // Save WebAuthn settings after successful user modification - if (response.data.saveStatus == 1) { + // Save WebAuthn settings after successful user modification (only if WebAuthn script loaded) + if (response.data.saveStatus == 1 && window.cyberPanelWebAuthn) { $scope.saveWebAuthnSettings(); } }, cantLoadInitialDatas); @@ -554,7 +599,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModified = false; $scope.canotModifyUser = false; // hide modify error on success $scope.couldNotConnect = true; - $scope.canotFetchDetails = true; + $scope.canotFetchDetails = false; // hide "Cannot fetch details" on save success $scope.detailsFetched = true; $scope.userAccountsLimit = true; $scope.accountTypeView = true; @@ -592,7 +637,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.couldNotConnect = false; $scope.canotFetchDetails = true; $scope.detailsFetched = true; - + $scope.errorMessage = (response && response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; } diff --git a/userManagment/homeDirectoryViews.py b/userManagment/homeDirectoryViews.py index 96f1403d4..55e8b4929 100644 --- a/userManagment/homeDirectoryViews.py +++ b/userManagment/homeDirectoryViews.py @@ -174,7 +174,9 @@ def getUserHomeDirectories(request): userID = request.session['userID'] currentACL = ACLManager.loadedACL(userID) - if currentACL['admin'] != 1 and currentACL['createNewUser'] != 1: + # Same visibility as create user page: admins, ACL editors, and users allowed to create accounts + if (currentACL['admin'] != 1 and currentACL['createNewUser'] != 1 + and currentACL.get('changeUserACL', 0) != 1): return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'}) # Get active home directories (tables home_directories / user_home_mappings may not exist yet) diff --git a/userManagment/templates/userManagment/createUser.html b/userManagment/templates/userManagment/createUser.html index 24e85da2d..b8332c9af 100644 --- a/userManagment/templates/userManagment/createUser.html +++ b/userManagment/templates/userManagment/createUser.html @@ -283,9 +283,11 @@
- {% for items in aclNames %} - + {% endfor %}

{% trans "Select the permission set for this user" %}

diff --git a/userManagment/views.py b/userManagment/views.py index 763f9a67d..6463fac54 100644 --- a/userManagment/views.py +++ b/userManagment/views.py @@ -4,6 +4,7 @@ from django.shortcuts import render, redirect from django.http import HttpResponse from django.db import models +from django.views.decorators.csrf import ensure_csrf_cookie from loginSystem.views import loadLoginPage from loginSystem.models import Administrator, ACL import json @@ -50,24 +51,31 @@ def viewProfile(request): return proc.render() +@ensure_csrf_cookie def createUser(request): userID = request.session['userID'] currentACL = ACLManager.loadedACL(userID) if currentACL['admin'] == 1: aclNames = ACLManager.unFileteredACLs() + default_acl = aclNames[0] if aclNames else 'user' proc = httpProc(request, 'userManagment/createUser.html', - {'aclNames': aclNames, 'securityLevels': SecurityLevel.list()}) + {'aclNames': aclNames, 'default_acl_name': default_acl, + 'securityLevels': SecurityLevel.list()}) return proc.render() elif currentACL['changeUserACL'] == 1: aclNames = ACLManager.unFileteredACLs() + default_acl = aclNames[0] if aclNames else 'user' proc = httpProc(request, 'userManagment/createUser.html', - {'aclNames': aclNames, 'securityLevels': SecurityLevel.list()}) + {'aclNames': aclNames, 'default_acl_name': default_acl, + 'securityLevels': SecurityLevel.list()}) return proc.render() elif currentACL['createNewUser'] == 1: aclNames = ['user'] + default_acl = 'user' proc = httpProc(request, 'userManagment/createUser.html', - {'aclNames': aclNames, 'securityLevels': SecurityLevel.list()}) + {'aclNames': aclNames, 'default_acl_name': default_acl, + 'securityLevels': SecurityLevel.list()}) return proc.render() else: return ACLManager.loadError() @@ -213,8 +221,18 @@ def submitUserCreation(request): email = data['email'] userName = data['userName'] password = data['password'] - websitesLimit = data['websitesLimit'] - selectedACL = data['selectedACL'] + try: + websitesLimit = int(data['websitesLimit']) + except (KeyError, TypeError, ValueError): + websitesLimit = 0 + selectedACL = data.get('selectedACL') + if selectedACL is None or (isinstance(selectedACL, str) and not selectedACL.strip()): + data_ret = {'status': 0, 'createStatus': 0, + 'error_message': 'Please select an access control list (ACL).'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data, content_type='application/json') + if isinstance(selectedACL, str): + selectedACL = selectedACL.strip() selectedHomeDirectory = data.get('selectedHomeDirectory', '') if ACLManager.CheckRegEx("^[\w'\-,.][^0-9_!¡?÷?¿/\\+=@#$%ˆ&*(){}|~<>;:[\]]{2,}$", firstName) == 0: