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 @@