fix(users): Create User ACL ng-model default and safer submitUserCreation

- Initialize selectedACL in create user template so JSON POST always includes ACL
- Explicit option values; pass default_acl_name from view for ng-init
- Coerce websitesLimit to int; validate selectedACL before ACL.objects.get
- ensure_csrf_cookie on createUser page load
- Optional /etc/cyberpanel/csrf_trusted_origins for HTTPS IP:port panels
- Allow changeUserACL on getUserHomeDirectories (parity with create user page)
- Sync public/static and static copies of userManagment.js with app static
This commit is contained in:
master3395
2026-03-25 01:10:11 +01:00
parent 2176147772
commit fb321340da
6 changed files with 641 additions and 114 deletions

View File

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

View File

@@ -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 + '<br>' +
'Full Name: ' + user.firstName + ' ' + user.lastName + '<br>' +
'Email: ' + user.email + '<br>' +
'ACL: ' + user.aclName + '<br>' +
'Token Status: ' + user.tokenStatus + '<br>' +
'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'

View File

@@ -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';
}

View File

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

View File

@@ -283,9 +283,11 @@
<div class="form-col-6">
<div class="form-group">
<label class="form-label">{% trans "Access Control List (ACL)" %}</label>
<select ng-model="selectedACL" class="form-control">
{# ng-init: Angular otherwise leaves selectedACL undefined until user picks an option, so POST omits ACL and user creation fails. #}
<select ng-model="selectedACL" class="form-control"
ng-init="selectedACL = selectedACL || '{{ default_acl_name|escapejs }}'">
{% for items in aclNames %}
<option>{{ items }}</option>
<option value="{{ items|escape }}">{{ items }}</option>
{% endfor %}
</select>
<p class="help-text">{% trans "Select the permission set for this user" %}</p>

View File

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