diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js
index 5d74f6e40..6dc11380c 100644
--- a/weed/admin/static/js/admin.js
+++ b/weed/admin/static/js/admin.js
@@ -2095,388 +2095,95 @@ function escapeHtml(text) {
return text.replace(/[&<>"']/g, function (m) { return map[m]; });
}
+
// ============================================================================
-// USER MANAGEMENT FUNCTIONS
+// SHARED MODAL UTILITIES FOR ACCESS KEY MANAGEMENT
// ============================================================================
-// Global variables for user management
-let currentEditingUser = '';
-let currentAccessKeysUser = '';
-
-// User Management Functions
-
-async function handleCreateUser() {
- const form = document.getElementById('createUserForm');
- const formData = new FormData(form);
-
- // Get selected actions
- const actionsSelect = document.getElementById('actions');
- const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value);
-
- const userData = {
- username: formData.get('username'),
- email: formData.get('email'),
- actions: selectedActions,
- generate_key: formData.get('generateKey') === 'on'
- };
-
- try {
- const response = await fetch('/api/users', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(userData)
- });
-
- if (response.ok) {
- const result = await response.json();
- showSuccessMessage('User created successfully');
-
- // Show the created access key if generated
- if (result.user && result.user.access_key) {
- showNewAccessKeyModal(result.user);
- }
-
- // Close modal and refresh page
- const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
- modal.hide();
- form.reset();
- setTimeout(() => window.location.reload(), 1000);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error creating user:', error);
- showErrorMessage('Failed to create user: ' + error.message);
- }
+// HTML escaping helper to prevent XSS
+function escapeHtmlForAttribute(text) {
+ if (!text) return '';
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
}
-async function editUser(username) {
- currentEditingUser = username;
-
- try {
- const response = await fetch(`/api/users/${username}`);
- if (response.ok) {
- const user = await response.json();
-
- // Populate edit form
- document.getElementById('editUsername').value = username;
- document.getElementById('editEmail').value = user.email || '';
-
- // Set selected actions
- const actionsSelect = document.getElementById('editActions');
- Array.from(actionsSelect.options).forEach(option => {
- option.selected = user.actions && user.actions.includes(option.value);
- });
-
- // Set selected policies
- const policiesSelect = document.getElementById('editPolicies');
- if (policiesSelect) {
- Array.from(policiesSelect.options).forEach(option => {
- option.selected = user.policy_names && user.policy_names.includes(option.value);
- });
- }
-
- // Show modal
- const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
- modal.show();
- } else {
- showErrorMessage('Failed to load user details');
- }
- } catch (error) {
- console.error('Error loading user:', error);
- showErrorMessage('Failed to load user details');
- }
-}
-
-async function handleUpdateUser() {
- const form = document.getElementById('editUserForm');
- const formData = new FormData(form);
-
- // Get selected actions
- const actionsSelect = document.getElementById('editActions');
- const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value);
-
- // Get selected policies
- const policiesSelect = document.getElementById('editPolicies');
- const selectedPolicies = policiesSelect ? Array.from(policiesSelect.selectedOptions).map(option => option.value) : [];
+function showModal(title, content) {
+ // Create a dynamic modal
+ const modalId = 'dynamicModal_' + Date.now();
- const userData = {
- email: formData.get('email'),
- actions: selectedActions,
- policy_names: selectedPolicies
- };
+ // Create modal structure using DOM to prevent XSS in title
+ const modalDiv = document.createElement('div');
+ modalDiv.className = 'modal fade';
+ modalDiv.id = modalId;
+ modalDiv.setAttribute('tabindex', '-1');
+ modalDiv.setAttribute('role', 'dialog');
- try {
- const response = await fetch(`/api/users/${currentEditingUser}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(userData)
- });
+ const modalDialog = document.createElement('div');
+ modalDialog.className = 'modal-dialog';
+ modalDialog.setAttribute('role', 'document');
- if (response.ok) {
- showSuccessMessage('User updated successfully');
+ const modalContent = document.createElement('div');
+ modalContent.className = 'modal-content';
- // Close modal and refresh page
- const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
- modal.hide();
- setTimeout(() => window.location.reload(), 1000);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error updating user:', error);
- showErrorMessage('Failed to update user: ' + error.message);
- }
-}
+ // Header
+ const modalHeader = document.createElement('div');
+ modalHeader.className = 'modal-header';
-function confirmDeleteUser(username) {
- confirmAction(
- `Are you sure you want to delete user "${username}"? This action cannot be undone.`,
- () => deleteUserConfirmed(username)
- );
-}
+ const modalTitle = document.createElement('h5');
+ modalTitle.className = 'modal-title';
+ modalTitle.textContent = title; // Safe - uses textContent
-function deleteUser(username) {
- confirmDeleteUser(username);
-}
+ const closeButton = document.createElement('button');
+ closeButton.type = 'button';
+ closeButton.className = 'btn-close';
+ closeButton.setAttribute('data-bs-dismiss', 'modal');
-async function deleteUserConfirmed(username) {
- try {
- const response = await fetch(`/api/users/${username}`, {
- method: 'DELETE'
- });
+ modalHeader.appendChild(modalTitle);
+ modalHeader.appendChild(closeButton);
- if (response.ok) {
- showSuccessMessage('User deleted successfully');
- setTimeout(() => window.location.reload(), 1000);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error deleting user:', error);
- showErrorMessage('Failed to delete user: ' + error.message);
- }
-}
+ // Body (content may contain HTML, so use innerHTML)
+ const modalBody = document.createElement('div');
+ modalBody.className = 'modal-body';
+ modalBody.innerHTML = content;
-async function showUserDetails(username) {
- try {
- const response = await fetch(`/api/users/${username}`);
- if (response.ok) {
- const user = await response.json();
+ // Footer
+ const modalFooter = document.createElement('div');
+ modalFooter.className = 'modal-footer';
- const content = createUserDetailsContent(user);
- document.getElementById('userDetailsContent').innerHTML = content;
+ const closeFooterButton = document.createElement('button');
+ closeFooterButton.type = 'button';
+ closeFooterButton.className = 'btn btn-secondary';
+ closeFooterButton.setAttribute('data-bs-dismiss', 'modal');
+ closeFooterButton.textContent = 'Close';
- const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
- modal.show();
- } else {
- showErrorMessage('Failed to load user details');
- }
- } catch (error) {
- console.error('Error loading user details:', error);
- showErrorMessage('Failed to load user details');
- }
-}
+ modalFooter.appendChild(closeFooterButton);
-function createUserDetailsContent(user) {
- return `
-
-
-
Basic Information
-
-
- Username:
- ${escapeHtml(user.username)}
-
-
- Email:
- ${escapeHtml(user.email || 'Not set')}
-
-
-
-
-
Permissions
-
- ${user.actions && user.actions.length > 0 ?
- user.actions.map(action => `${action} `).join('') :
- 'No permissions assigned '
- }
-
-
-
Access Keys
- ${user.access_keys && user.access_keys.length > 0 ?
- createAccessKeysTable(user.access_keys) :
- '
No access keys
'
- }
-
-
- `;
-}
+ // Assemble modal
+ modalContent.appendChild(modalHeader);
+ modalContent.appendChild(modalBody);
+ modalContent.appendChild(modalFooter);
+ modalDialog.appendChild(modalContent);
+ modalDiv.appendChild(modalDialog);
-function createAccessKeysTable(accessKeys) {
- return `
-
-
-
-
- Access Key
- Created
-
-
-
- ${accessKeys.map(key => `
-
- ${key.access_key}
- ${new Date(key.created_at).toLocaleDateString()}
-
- `).join('')}
-
-
-
- `;
-}
-
-async function manageAccessKeys(username) {
- currentAccessKeysUser = username;
- document.getElementById('accessKeysUsername').textContent = username;
-
- await loadAccessKeys(username);
+ // Add modal to body
+ document.body.appendChild(modalDiv);
- const modal = new bootstrap.Modal(document.getElementById('accessKeysModal'));
+ // Show modal
+ const modal = new bootstrap.Modal(document.getElementById(modalId));
modal.show();
-}
-
-async function loadAccessKeys(username) {
- try {
- const response = await fetch(`/api/users/${username}`);
- if (response.ok) {
- const user = await response.json();
-
- const content = createAccessKeysManagementContent(user.access_keys || []);
- document.getElementById('accessKeysContent').innerHTML = content;
- } else {
- document.getElementById('accessKeysContent').innerHTML = 'Failed to load access keys
';
- }
- } catch (error) {
- console.error('Error loading access keys:', error);
- document.getElementById('accessKeysContent').innerHTML = 'Error loading access keys
';
- }
-}
-
-function createAccessKeysManagementContent(accessKeys) {
- if (accessKeys.length === 0) {
- return 'No access keys found. Create one to get started.
';
- }
-
- return `
-
-
-
-
- Access Key
- Secret Key
- Created
- Actions
-
-
-
- ${accessKeys.map(key => `
-
-
- ${key.access_key}
-
-
-
-
-
- ••••••••••••••••
-
-
-
-
- ${new Date(key.created_at).toLocaleDateString()}
-
-
-
-
-
-
- `).join('')}
-
-
-
- `;
-}
-
-async function createAccessKey() {
- if (!currentAccessKeysUser) {
- showErrorMessage('No user selected');
- return;
- }
-
- try {
- const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- }
- });
-
- if (response.ok) {
- const result = await response.json();
- showSuccessMessage('Access key created successfully');
-
- // Show the new access key
- showNewAccessKeyModal(result.access_key);
-
- // Reload access keys
- await loadAccessKeys(currentAccessKeysUser);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error creating access key:', error);
- showErrorMessage('Failed to create access key: ' + error.message);
- }
-}
-
-function confirmDeleteAccessKey(accessKeyId) {
- confirmAction(
- `Are you sure you want to delete access key "${accessKeyId}"? This action cannot be undone.`,
- () => deleteAccessKeyConfirmed(accessKeyId)
- );
-}
-
-async function deleteAccessKeyConfirmed(accessKeyId) {
- try {
- const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys/${accessKeyId}`, {
- method: 'DELETE'
- });
-
- if (response.ok) {
- showSuccessMessage('Access key deleted successfully');
- // Reload access keys
- await loadAccessKeys(currentAccessKeysUser);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error deleting access key:', error);
- showErrorMessage('Failed to delete access key: ' + error.message);
- }
+ // Remove modal from DOM when hidden
+ document.getElementById(modalId).addEventListener('hidden.bs.modal', function () {
+ this.remove();
+ });
}
function showSecretKey(accessKey, secretKey) {
+ const modalId = 'secretKeyModal_' + Date.now();
+ const escapedAccessKey = escapeHtmlForAttribute(accessKey);
+ const escapedSecretKey = escapeHtmlForAttribute(secretKey);
+
const content = `
@@ -2485,8 +2192,8 @@ function showSecretKey(accessKey, secretKey) {
Access Key:
-
-
+
+
@@ -2494,8 +2201,8 @@ function showSecretKey(accessKey, secretKey) {
Secret Key:
-
-
+
+
@@ -2506,20 +2213,20 @@ function showSecretKey(accessKey, secretKey) {
}
function showNewAccessKeyModal(accessKeyData) {
+ const modalId = 'newKeyModal_' + Date.now();
+ const escapedAccessKey = escapeHtmlForAttribute(accessKeyData.access_key);
+ const escapedSecretKey = escapeHtmlForAttribute(accessKeyData.secret_key);
+
const content = `
Success! Your new access key has been created.
-
-
- Important: These credentials provide access to your object storage. Keep them secure and don't share them. You can view them again through the user management interface if needed.
-
Access Key:
-
-
+
+
@@ -2527,8 +2234,8 @@ function showNewAccessKeyModal(accessKeyData) {
Secret Key:
-
-
+
+
@@ -2538,40 +2245,32 @@ function showNewAccessKeyModal(accessKeyData) {
showModal('New Access Key Created', content);
}
-function showModal(title, content) {
- // Create a dynamic modal
- const modalId = 'dynamicModal_' + Date.now();
- const modalHtml = `
-
- `;
-
- // Add modal to body
- document.body.insertAdjacentHTML('beforeend', modalHtml);
-
- // Show modal
- const modal = new bootstrap.Modal(document.getElementById(modalId));
- modal.show();
+// Helper function to copy from an input field
+function copyFromInput(inputId) {
+ const input = document.getElementById(inputId);
+ if (input) {
+ input.select();
+ input.setSelectionRange(0, 99999); // For mobile devices
- // Remove modal from DOM when hidden
- document.getElementById(modalId).addEventListener('hidden.bs.modal', function () {
- this.remove();
- });
+ try {
+ const successful = document.execCommand('copy');
+ if (successful) {
+ showAlert('success', 'Copied to clipboard!');
+ } else {
+ // Try modern clipboard API as fallback
+ navigator.clipboard.writeText(input.value).then(() => {
+ showAlert('success', 'Copied to clipboard!');
+ }).catch(() => {
+ showAlert('danger', 'Failed to copy');
+ });
+ }
+ } catch (err) {
+ // Try modern clipboard API as fallback
+ navigator.clipboard.writeText(input.value).then(() => {
+ showAlert('success', 'Copied to clipboard!');
+ }).catch(() => {
+ showAlert('danger', 'Failed to copy');
+ });
+ }
+ }
}
-
-
-
diff --git a/weed/admin/view/app/object_store_users.templ b/weed/admin/view/app/object_store_users.templ
index e93f39d75..0f9a2d693 100644
--- a/weed/admin/view/app/object_store_users.templ
+++ b/weed/admin/view/app/object_store_users.templ
@@ -223,6 +223,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
Hold Ctrl/Cmd to select multiple permissions
+
+
Bucket Scope
+
Apply selected permissions to specific buckets or all buckets
+
+
+
+
+ All Buckets
+
+
+
+
+
+ Specific Buckets
+
+
+
+
+
+
+
+ Hold Ctrl/Cmd to select multiple buckets
+
+
Attached Policies
@@ -282,6 +306,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
+
+
Bucket Scope
+
Apply selected permissions to specific buckets or all buckets
+
+
+
+
+ All Buckets
+
+
+
+
+
+ Specific Buckets
+
+
+
+
+
+
+
+ Hold Ctrl/Cmd to select multiple buckets
+
+
Attached Policies
@@ -386,8 +434,35 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
// Load policies for dropdowns
loadPolicies();
+
+ // Load buckets for bucket permissions
+ loadBuckets();
});
+ // Global variable to store available buckets
+ var availableBuckets = [];
+ var bucketPermissionCounter = 0;
+
+ // Load buckets
+ async function loadBuckets() {
+ try {
+ const response = await fetch('/api/s3/buckets');
+ if (response.ok) {
+ const data = await response.json();
+ availableBuckets = data.buckets || [];
+ console.log('Loaded', availableBuckets.length, 'buckets');
+ // Populate bucket selection dropdowns
+ populateBucketSelections();
+ } else {
+ console.warn('Failed to load buckets');
+ availableBuckets = [];
+ }
+ } catch (error) {
+ console.error('Error loading buckets:', error);
+ availableBuckets = [];
+ }
+ }
+
// Load policies
async function loadPolicies() {
try {
@@ -434,6 +509,170 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
}
}
+ // Toggle bucket permission fields when Admin checkbox changes
+ function toggleBucketPermissionFields(mode) {
+ mode = mode || 'create';
+ const adminCheckbox = document.getElementById(mode === 'edit' ? 'editBucketAdmin' : 'bucketAdmin');
+ const permissionFields = document.getElementById(mode === 'edit' ? 'editBucketPermissionFields' : 'bucketPermissionFields');
+
+ if (adminCheckbox && permissionFields) {
+ permissionFields.style.display = adminCheckbox.checked ? 'none' : 'block';
+ }
+ }
+
+ // Toggle bucket list visibility when bucket scope changes
+ function toggleBucketList(mode) {
+ mode = mode || 'create';
+ const specificRadio = document.getElementById(mode === 'edit' ? 'editSpecificBuckets' : 'specificBuckets');
+ const bucketList = document.getElementById(mode === 'edit' ? 'editBucketSelectionList' : 'bucketSelectionList');
+
+ if (specificRadio && bucketList) {
+ bucketList.style.display = specificRadio.checked ? 'block' : 'none';
+ }
+ }
+
+ // Populate bucket selection dropdowns
+ function populateBucketSelections() {
+ const createSelect = document.getElementById('selectedBuckets');
+ const editSelect = document.getElementById('editSelectedBuckets');
+
+ [createSelect, editSelect].forEach(select => {
+ if (select) {
+ select.innerHTML = '';
+ availableBuckets.forEach(bucket => {
+ const option = document.createElement('option');
+ option.value = bucket.name;
+ option.textContent = bucket.name;
+ select.appendChild(option);
+ });
+ }
+ });
+ }
+
+ // Parse bucket permissions from actions array for new UI
+ function parseBucketPermissions(actions) {
+ const result = {
+ isAdmin: false,
+ permissions: [],
+ applyToAll: false,
+ specificBuckets: []
+ };
+
+ // Check if user has Admin permission
+ if (actions.includes('Admin')) {
+ result.isAdmin = true;
+ return result;
+ }
+
+ // Separate bucket-scoped from global actions
+ const bucketActions = [];
+ const globalBucketPerms = [];
+
+ actions.forEach(action => {
+ if (action.includes(':')) {
+ const parts = action.split(':');
+ const perm = parts[0];
+ const bucket = parts.slice(1).join(':').replace(/\/\*$/, '');
+ bucketActions.push({ permission: perm, bucket: bucket });
+ } else {
+ globalBucketPerms.push(action);
+ }
+ });
+
+ // If we have global bucket permissions (no colon), they apply to all buckets
+ if (globalBucketPerms.length > 0) {
+ result.permissions = globalBucketPerms;
+ result.applyToAll = true;
+ } else if (bucketActions.length > 0) {
+ // Get unique permissions and buckets
+ const perms = [...new Set(bucketActions.map(ba => ba.permission))];
+ const buckets = [...new Set(bucketActions.map(ba => ba.bucket))];
+
+ result.permissions = perms;
+ result.applyToAll = false;
+ result.specificBuckets = buckets;
+ }
+
+ return result;
+ }
+
+ // Build bucket permission action strings using original permissions dropdown
+ /**
+ * Builds bucket permission strings based on selected permissions and bucket scope.
+ * @param {string} mode - The operation mode, either 'create' or 'edit'.
+ * @returns {string[]|null} Array of permission strings (e.g., ['Read:bucket1']) or null if validation fails (specific scope selected but no buckets).
+ */
+ function buildBucketPermissions(mode) {
+ mode = mode || 'create';
+ const selectId = mode === 'edit' ? 'editActions' : 'actions';
+ const permSelect = document.getElementById(selectId);
+
+ if (!permSelect) return [];
+
+ // Get selected permissions from the original multi-select
+ const selectedPerms = Array.from(permSelect.selectedOptions).map(opt => opt.value);
+
+ // If Admin is selected, return just Admin (it overrides everything)
+ if (selectedPerms.includes('Admin')) {
+ return ['Admin'];
+ }
+
+ if (selectedPerms.length === 0) {
+ return [];
+ }
+
+ // Check if applying to all buckets or specific ones
+ // Use querySelector to find the checked radio button by name group
+ const scopeName = mode === 'edit' ? 'editBucketScope' : 'bucketScope';
+
+ // Try multiple methods to find the checked radio
+ let checkedRadio = document.querySelector(`input[name="${scopeName}"]:checked`);
+
+ // Fallback: check both radio buttons explicitly
+ if (!checkedRadio) {
+ const allBucketsId = mode === 'edit' ? 'editAllBuckets' : 'allBuckets';
+ const specificBucketsId = mode === 'edit' ? 'editSpecificBuckets' : 'specificBuckets';
+
+ const allBucketsRadio = document.getElementById(allBucketsId);
+ const specificBucketsRadio = document.getElementById(specificBucketsId);
+
+ if (specificBucketsRadio && specificBucketsRadio.checked) {
+ checkedRadio = specificBucketsRadio;
+ } else if (allBucketsRadio && allBucketsRadio.checked) {
+ checkedRadio = allBucketsRadio;
+ }
+ }
+
+ // Default to 'all' if nothing is checked (shouldn't happen) or if 'all' is checked
+ const applyToAll = !checkedRadio || checkedRadio.value === 'all';
+
+ if (applyToAll) {
+ // Return global permissions (no bucket specification)
+ return selectedPerms;
+ } else {
+ // Get selected specific buckets
+ const bucketSelect = document.getElementById(mode === 'edit' ? 'editSelectedBuckets' : 'selectedBuckets');
+ if (!bucketSelect) return null;
+
+ const selectedBuckets = Array.from(bucketSelect.selectedOptions).map(opt => opt.value);
+
+ // Return null to signal validation failure if no buckets selected
+ if (selectedBuckets.length === 0) {
+ return null;
+ }
+
+ // Build bucket-scoped permissions
+ const actions = [];
+ selectedPerms.forEach(perm => {
+ selectedBuckets.forEach(bucket => {
+ actions.push(perm + ':' + bucket);
+ });
+ });
+
+ return actions;
+ }
+ }
+
// Show user details modal
async function showUserDetails(username) {
try {
@@ -477,6 +716,44 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
});
}
+ // Populate bucket permissions using original permissions dropdown
+ if (user.actions && user.actions.length > 0) {
+ const bucketPerms = parseBucketPermissions(user.actions);
+
+ // Set permissions in the original multi-select
+ const actionsSelect = document.getElementById('editActions');
+ if (actionsSelect) {
+ Array.from(actionsSelect.options).forEach(option => {
+ if (bucketPerms.isAdmin && option.value === 'Admin') {
+ option.selected = true;
+ } else if (!bucketPerms.isAdmin && bucketPerms.permissions.includes(option.value)) {
+ option.selected = true;
+ }
+ });
+ }
+
+ // Set bucket scope (all or specific)
+ const allBucketsRadio = document.getElementById('editAllBuckets');
+ const specificBucketsRadio = document.getElementById('editSpecificBuckets');
+
+ if (!bucketPerms.isAdmin) {
+ if (bucketPerms.applyToAll) {
+ if (allBucketsRadio) allBucketsRadio.checked = true;
+ } else if (bucketPerms.specificBuckets.length > 0) {
+ if (specificBucketsRadio) specificBucketsRadio.checked = true;
+ toggleBucketList('edit');
+
+ // Select specific buckets
+ const bucketSelect = document.getElementById('editSelectedBuckets');
+ if (bucketSelect) {
+ Array.from(bucketSelect.options).forEach(option => {
+ option.selected = bucketPerms.specificBuckets.includes(option.value);
+ });
+ }
+ }
+ }
+ }
+
// Show modal
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
@@ -535,10 +812,13 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
const form = document.getElementById('createUserForm');
const formData = new FormData(form);
+ // Get permissions with bucket scope applied
+ const allActions = buildBucketPermissions('create');
+
const userData = {
username: formData.get('username'),
email: formData.get('email'),
- actions: Array.from(document.getElementById('actions').selectedOptions).map(option => option.value),
+ actions: allActions,
policy_names: Array.from(document.getElementById('policies').selectedOptions).map(option => option.value),
generate_key: document.getElementById('generateKey').checked
};
@@ -577,6 +857,62 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
}
+ // Handle update user form submission
+ async function handleUpdateUser() {
+ const username = document.getElementById('editUsername').value;
+ if (!username) {
+ showErrorMessage('Username is required');
+ return;
+ }
+
+ // Get permissions with bucket scope applied
+ const allActions = buildBucketPermissions('edit');
+
+ // Validate that permissions are not empty
+ if (!allActions || allActions.length === 0) {
+ showErrorMessage('At least one permission must be selected');
+ return;
+ }
+
+ // Check for null (validation failure from buildBucketPermissionsNew)
+ if (allActions === null) {
+ showErrorMessage('Please select at least one bucket when using specific bucket permissions');
+ return;
+ }
+
+ const userData = {
+ email: document.getElementById('editEmail').value,
+ actions: allActions,
+ policy_names: Array.from(document.getElementById('editPolicies').selectedOptions).map(option => option.value)
+ };
+
+ try {
+ const response = await fetch(`/api/users/${username}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(userData)
+ });
+
+ if (response.ok) {
+ showSuccessMessage('User updated successfully');
+
+ // Close modal and refresh page
+ const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
+ modal.hide();
+ setTimeout(() => window.location.reload(), 1000);
+ } else {
+ const error = await response.json();
+ showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error'));
+ }
+ } catch (error) {
+ console.error('Error updating user:', error);
+ showErrorMessage('Failed to update user: ' + error.message);
+ }
+ }
+
+
// Create user details content
function createUserDetailsContent(user) {
var detailsHtml = '';
@@ -639,6 +975,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
keysHtml += '
' + escapeHtml(key.access_key) + '';
keysHtml += '
Active ';
keysHtml += '
';
+ // Add "View Secret" button with data attributes
+ keysHtml += '';
+ keysHtml += ' View Secret';
+ keysHtml += ' ';
+ // Delete button
keysHtml += '';
keysHtml += ' Delete';
keysHtml += ' ';
@@ -649,6 +990,18 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
keysHtml += '';
keysHtml += '';
keysHtml += '';
+
+ // Add delegated event listener for view secret buttons
+ setTimeout(() => {
+ document.querySelectorAll('.view-secret-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const accessKey = this.getAttribute('data-access-key');
+ const secretKey = this.getAttribute('data-secret-key');
+ showSecretKey(accessKey, secretKey);
+ });
+ });
+ }, 100);
+
return keysHtml;
}
@@ -667,6 +1020,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
if (response.ok) {
const result = await response.json();
+
+ // Show the new access key details (IMPORTANT: secret key is only shown once!)
+ if (result.access_key) {
+ showNewAccessKeyModal(result.access_key);
+ }
+
showSuccessMessage('Access key created successfully');
// Refresh access keys display
@@ -713,16 +1072,6 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
}
}
- // Show new access key modal (when user is created with generated key)
- function showNewAccessKeyModal(user) {
- // Create a simple alert for now - could be enhanced with a dedicated modal
- var message = 'New user created!\n\n';
- message += 'Username: ' + user.username + '\n';
- message += 'Access Key: ' + user.access_key + '\n';
- message += 'Secret Key: ' + user.secret_key + '\n\n';
- message += 'Please save these credentials securely.';
- alert(message);
- }
// Utility functions
function showSuccessMessage(message) {
diff --git a/weed/admin/view/app/object_store_users_templ.go b/weed/admin/view/app/object_store_users_templ.go
index c60ae84fe..1090e41e6 100644
--- a/weed/admin/view/app/object_store_users_templ.go
+++ b/weed/admin/view/app/object_store_users_templ.go
@@ -193,7 +193,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Access Keys for Create New Key
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Access Keys for Create New Key
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}