window.IAMManagement = (function() { 'use strict'; var users = []; var currentUserKey = null; var endpoints = {}; var csrfToken = ''; var iamLocked = false; var policyModal = null; var editUserModal = null; var deleteUserModal = null; var rotateSecretModal = null; var expiryModal = null; var currentRotateKey = null; var currentEditKey = null; var currentDeleteKey = null; var currentExpiryKey = null; var ALL_S3_ACTIONS = [ 'list', 'read', 'write', 'delete', 'share', 'policy', 'replication', 'lifecycle', 'cors', 'create_bucket', 'delete_bucket', 'versioning', 'tagging', 'encryption', 'quota', 'object_lock', 'notification', 'logging', 'website' ]; var policyTemplates = { full: [{ bucket: '*', actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'create_bucket', 'delete_bucket', 'replication', 'lifecycle', 'cors', 'versioning', 'tagging', 'encryption', 'quota', 'object_lock', 'notification', 'logging', 'website', 'iam:*'] }], readonly: [{ bucket: '*', actions: ['list', 'read'] }], writer: [{ bucket: '*', actions: ['list', 'read', 'write'] }], operator: [{ bucket: '*', actions: ['list', 'read', 'write', 'delete', 'create_bucket', 'delete_bucket'] }], bucketadmin: [{ bucket: '*', actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'create_bucket', 'delete_bucket', 'versioning', 'tagging', 'encryption', 'cors', 'lifecycle', 'quota', 'object_lock', 'notification', 'logging', 'website', 'replication'] }] }; function isAdminUser(policies) { if (!policies || !policies.length) return false; return policies.some(function(p) { return p.actions && (p.actions.indexOf('iam:*') >= 0 || p.actions.indexOf('*') >= 0); }); } function getPermissionLevel(actions) { if (!actions || !actions.length) return 'Custom (0)'; if (actions.indexOf('*') >= 0) return 'Full Access'; if (actions.length >= ALL_S3_ACTIONS.length) { var hasAll = ALL_S3_ACTIONS.every(function(a) { return actions.indexOf(a) >= 0; }); if (hasAll) return 'Full Access'; } var has = function(a) { return actions.indexOf(a) >= 0; }; if (has('list') && has('read') && has('write') && has('delete')) return 'Read + Write + Delete'; if (has('list') && has('read') && has('write')) return 'Read + Write'; if (has('list') && has('read')) return 'Read Only'; return 'Custom (' + actions.length + ')'; } function getBucketLabel(bucket) { return bucket === '*' ? 'All Buckets' : bucket; } function init(config) { users = config.users || []; currentUserKey = config.currentUserKey || null; endpoints = config.endpoints || {}; csrfToken = config.csrfToken || ''; iamLocked = config.iamLocked || false; if (iamLocked) return; initModals(); setupJsonAutoIndent(); setupCopyButtons(); setupPolicyEditor(); setupCreateUserModal(); setupEditUserModal(); setupDeleteUserModal(); setupRotateSecretModal(); setupExpiryModal(); setupFormHandlers(); setupSearch(); setupCopyAccessKeyButtons(); } function initModals() { var policyModalEl = document.getElementById('policyEditorModal'); var editModalEl = document.getElementById('editUserModal'); var deleteModalEl = document.getElementById('deleteUserModal'); var rotateModalEl = document.getElementById('rotateSecretModal'); var expiryModalEl = document.getElementById('expiryModal'); if (policyModalEl) policyModal = new bootstrap.Modal(policyModalEl); if (editModalEl) editUserModal = new bootstrap.Modal(editModalEl); if (deleteModalEl) deleteUserModal = new bootstrap.Modal(deleteModalEl); if (rotateModalEl) rotateSecretModal = new bootstrap.Modal(rotateModalEl); if (expiryModalEl) expiryModal = new bootstrap.Modal(expiryModalEl); } function setupJsonAutoIndent() { window.UICore.setupJsonAutoIndent(document.getElementById('policyEditorDocument')); window.UICore.setupJsonAutoIndent(document.getElementById('createUserPolicies')); } function setupCopyButtons() { document.querySelectorAll('.config-copy').forEach(function(button) { button.addEventListener('click', async function() { var targetId = button.dataset.copyTarget; var target = document.getElementById(targetId); if (!target) return; await window.UICore.copyToClipboard(target.innerText, button, 'Copy JSON'); }); }); var accessKeyCopyButton = document.querySelector('[data-access-key-copy]'); if (accessKeyCopyButton) { accessKeyCopyButton.addEventListener('click', async function() { var accessKeyInput = document.getElementById('disclosedAccessKeyValue'); if (!accessKeyInput) return; await window.UICore.copyToClipboard(accessKeyInput.value, accessKeyCopyButton, 'Copy'); }); } var secretCopyButton = document.querySelector('[data-secret-copy]'); if (secretCopyButton) { secretCopyButton.addEventListener('click', async function() { var secretInput = document.getElementById('disclosedSecretValue'); if (!secretInput) return; await window.UICore.copyToClipboard(secretInput.value, secretCopyButton, 'Copy'); }); } } function getUserPolicies(accessKey) { var user = users.find(function(u) { return u.access_key === accessKey; }); return user ? JSON.stringify(user.policies, null, 2) : ''; } function applyPolicyTemplate(name, textareaEl) { if (policyTemplates[name] && textareaEl) { textareaEl.value = JSON.stringify(policyTemplates[name], null, 2); } } function setupPolicyEditor() { var userLabelEl = document.getElementById('policyEditorUserLabel'); var userInputEl = document.getElementById('policyEditorUser'); var textareaEl = document.getElementById('policyEditorDocument'); document.querySelectorAll('[data-policy-template]').forEach(function(button) { button.addEventListener('click', function() { applyPolicyTemplate(button.dataset.policyTemplate, textareaEl); }); }); document.querySelectorAll('[data-policy-editor]').forEach(function(button) { button.addEventListener('click', function() { var key = button.getAttribute('data-access-key'); if (!key) return; userLabelEl.textContent = key; userInputEl.value = key; textareaEl.value = getUserPolicies(key); policyModal.show(); }); }); } function generateSecureHex(byteCount) { var arr = new Uint8Array(byteCount); crypto.getRandomValues(arr); return Array.from(arr).map(function(b) { return b.toString(16).padStart(2, '0'); }).join(''); } function generateSecureBase64(byteCount) { var arr = new Uint8Array(byteCount); crypto.getRandomValues(arr); var binary = ''; for (var i = 0; i < arr.length; i++) { binary += String.fromCharCode(arr[i]); } return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } function setupCreateUserModal() { var createUserPoliciesEl = document.getElementById('createUserPolicies'); document.querySelectorAll('[data-create-policy-template]').forEach(function(button) { button.addEventListener('click', function() { applyPolicyTemplate(button.dataset.createPolicyTemplate, createUserPoliciesEl); }); }); var genAccessKeyBtn = document.getElementById('generateAccessKeyBtn'); if (genAccessKeyBtn) { genAccessKeyBtn.addEventListener('click', function() { var input = document.getElementById('createUserAccessKey'); if (input) input.value = generateSecureHex(8); }); } var genSecretKeyBtn = document.getElementById('generateSecretKeyBtn'); if (genSecretKeyBtn) { genSecretKeyBtn.addEventListener('click', function() { var input = document.getElementById('createUserSecretKey'); if (input) input.value = generateSecureBase64(24); }); } } function setupEditUserModal() { var editUserForm = document.getElementById('editUserForm'); var editUserDisplayName = document.getElementById('editUserDisplayName'); document.querySelectorAll('[data-edit-user]').forEach(function(btn) { btn.addEventListener('click', function() { var key = btn.dataset.editUser; var name = btn.dataset.displayName; currentEditKey = key; editUserDisplayName.value = name; editUserForm.action = endpoints.updateUser.replace('ACCESS_KEY', key); editUserModal.show(); }); }); } function setupDeleteUserModal() { var deleteUserForm = document.getElementById('deleteUserForm'); var deleteUserLabel = document.getElementById('deleteUserLabel'); var deleteSelfWarning = document.getElementById('deleteSelfWarning'); document.querySelectorAll('[data-delete-user]').forEach(function(btn) { btn.addEventListener('click', function() { var key = btn.dataset.deleteUser; currentDeleteKey = key; deleteUserLabel.textContent = key; deleteUserForm.action = endpoints.deleteUser.replace('ACCESS_KEY', key); if (key === currentUserKey) { deleteSelfWarning.classList.remove('d-none'); } else { deleteSelfWarning.classList.add('d-none'); } deleteUserModal.show(); }); }); } function setupRotateSecretModal() { var rotateUserLabel = document.getElementById('rotateUserLabel'); var confirmRotateBtn = document.getElementById('confirmRotateBtn'); var rotateCancelBtn = document.getElementById('rotateCancelBtn'); var rotateDoneBtn = document.getElementById('rotateDoneBtn'); var rotateSecretConfirm = document.getElementById('rotateSecretConfirm'); var rotateSecretResult = document.getElementById('rotateSecretResult'); var newSecretKeyInput = document.getElementById('newSecretKey'); var copyNewSecretBtn = document.getElementById('copyNewSecret'); document.querySelectorAll('[data-rotate-user]').forEach(function(btn) { btn.addEventListener('click', function() { currentRotateKey = btn.dataset.rotateUser; rotateUserLabel.textContent = currentRotateKey; rotateSecretConfirm.classList.remove('d-none'); rotateSecretResult.classList.add('d-none'); confirmRotateBtn.classList.remove('d-none'); rotateCancelBtn.classList.remove('d-none'); rotateDoneBtn.classList.add('d-none'); rotateSecretModal.show(); }); }); if (confirmRotateBtn) { confirmRotateBtn.addEventListener('click', async function() { if (!currentRotateKey) return; window.UICore.setButtonLoading(confirmRotateBtn, true, 'Rotating...'); try { var url = endpoints.rotateSecret.replace('ACCESS_KEY', currentRotateKey); var response = await fetch(url, { method: 'POST', headers: { 'Accept': 'application/json', 'X-CSRFToken': csrfToken } }); if (!response.ok) { var data = await response.json(); throw new Error(data.error || 'Failed to rotate secret'); } var data = await response.json(); newSecretKeyInput.value = data.secret_key; rotateSecretConfirm.classList.add('d-none'); rotateSecretResult.classList.remove('d-none'); confirmRotateBtn.classList.add('d-none'); rotateCancelBtn.classList.add('d-none'); rotateDoneBtn.classList.remove('d-none'); } catch (err) { if (window.showToast) { window.showToast(err.message, 'Error', 'danger'); } rotateSecretModal.hide(); } finally { window.UICore.setButtonLoading(confirmRotateBtn, false); } }); } if (copyNewSecretBtn) { copyNewSecretBtn.addEventListener('click', async function() { await window.UICore.copyToClipboard(newSecretKeyInput.value, copyNewSecretBtn, 'Copy'); }); } if (rotateDoneBtn) { rotateDoneBtn.addEventListener('click', function() { window.location.reload(); }); } } function openExpiryModal(key, expiresAt) { currentExpiryKey = key; var label = document.getElementById('expiryUserLabel'); var input = document.getElementById('expiryDateInput'); var form = document.getElementById('expiryForm'); if (label) label.textContent = key; if (expiresAt) { try { var dt = new Date(expiresAt); var local = new Date(dt.getTime() - dt.getTimezoneOffset() * 60000); if (input) input.value = local.toISOString().slice(0, 16); } catch(e) { if (input) input.value = ''; } } else { if (input) input.value = ''; } if (form) form.action = endpoints.updateExpiry.replace('ACCESS_KEY', key); var modalEl = document.getElementById('expiryModal'); if (modalEl) { var modal = bootstrap.Modal.getOrCreateInstance(modalEl); modal.show(); } } function setupExpiryModal() { document.querySelectorAll('[data-expiry-user]').forEach(function(btn) { btn.addEventListener('click', function(e) { e.preventDefault(); openExpiryModal(btn.dataset.expiryUser, btn.dataset.expiresAt || ''); }); }); document.querySelectorAll('[data-expiry-preset]').forEach(function(btn) { btn.addEventListener('click', function() { var preset = btn.dataset.expiryPreset; var input = document.getElementById('expiryDateInput'); if (!input) return; if (preset === 'clear') { input.value = ''; return; } var now = new Date(); var ms = 0; if (preset === '1h') ms = 3600000; else if (preset === '24h') ms = 86400000; else if (preset === '7d') ms = 7 * 86400000; else if (preset === '30d') ms = 30 * 86400000; else if (preset === '90d') ms = 90 * 86400000; var future = new Date(now.getTime() + ms); var local = new Date(future.getTime() - future.getTimezoneOffset() * 60000); input.value = local.toISOString().slice(0, 16); }); }); var expiryForm = document.getElementById('expiryForm'); if (expiryForm) { expiryForm.addEventListener('submit', function(e) { e.preventDefault(); window.UICore.submitFormAjax(expiryForm, { successMessage: 'Expiry updated', onSuccess: function() { var modalEl = document.getElementById('expiryModal'); if (modalEl) bootstrap.Modal.getOrCreateInstance(modalEl).hide(); window.location.reload(); } }); }); } } function createUserCardHtml(accessKey, displayName, policies) { var admin = isAdminUser(policies); var cardClass = 'card h-100 iam-user-card' + (admin ? ' iam-admin-card' : ''); var roleBadge = admin ? 'Admin' : 'User'; var policyBadges = ''; if (policies && policies.length > 0) { policyBadges = policies.map(function(p) { var bucketLabel = getBucketLabel(p.bucket); var permLevel = getPermissionLevel(p.actions); return '' + '' + '' + '' + window.UICore.escapeHtml(bucketLabel) + ' · ' + window.UICore.escapeHtml(permLevel) + ''; }).join(''); } else { policyBadges = 'No policies'; } var esc = window.UICore.escapeHtml; return '
' + '
' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
' + esc(displayName) + '
' + roleBadge + '
' + '
' + '' + esc(accessKey) + '' + '' + '
' + '
' + '
' + '
' + '
Bucket Permissions
' + '
' + policyBadges + '
' + '' + '
'; } function attachUserCardHandlers(cardElement, accessKey, displayName) { var editBtn = cardElement.querySelector('[data-edit-user]'); if (editBtn) { editBtn.addEventListener('click', function() { currentEditKey = accessKey; document.getElementById('editUserDisplayName').value = displayName; document.getElementById('editUserForm').action = endpoints.updateUser.replace('ACCESS_KEY', accessKey); editUserModal.show(); }); } var deleteBtn = cardElement.querySelector('[data-delete-user]'); if (deleteBtn) { deleteBtn.addEventListener('click', function() { currentDeleteKey = accessKey; document.getElementById('deleteUserLabel').textContent = accessKey; document.getElementById('deleteUserForm').action = endpoints.deleteUser.replace('ACCESS_KEY', accessKey); var deleteSelfWarning = document.getElementById('deleteSelfWarning'); if (accessKey === currentUserKey) { deleteSelfWarning.classList.remove('d-none'); } else { deleteSelfWarning.classList.add('d-none'); } deleteUserModal.show(); }); } var rotateBtn = cardElement.querySelector('[data-rotate-user]'); if (rotateBtn) { rotateBtn.addEventListener('click', function() { currentRotateKey = accessKey; document.getElementById('rotateUserLabel').textContent = accessKey; document.getElementById('rotateSecretConfirm').classList.remove('d-none'); document.getElementById('rotateSecretResult').classList.add('d-none'); document.getElementById('confirmRotateBtn').classList.remove('d-none'); document.getElementById('rotateCancelBtn').classList.remove('d-none'); document.getElementById('rotateDoneBtn').classList.add('d-none'); rotateSecretModal.show(); }); } var expiryBtn = cardElement.querySelector('[data-expiry-user]'); if (expiryBtn) { expiryBtn.addEventListener('click', function(e) { e.preventDefault(); openExpiryModal(accessKey, ''); }); } var policyBtn = cardElement.querySelector('[data-policy-editor]'); if (policyBtn) { policyBtn.addEventListener('click', function() { document.getElementById('policyEditorUserLabel').textContent = accessKey; document.getElementById('policyEditorUser').value = accessKey; document.getElementById('policyEditorDocument').value = getUserPolicies(accessKey); policyModal.show(); }); } var copyBtn = cardElement.querySelector('[data-copy-access-key]'); if (copyBtn) { copyBtn.addEventListener('click', function() { copyAccessKey(copyBtn); }); } } function updateUserCount() { var countEl = document.querySelector('.card-header .text-muted.small'); if (countEl) { var count = document.querySelectorAll('.iam-user-card').length; countEl.textContent = count + ' user' + (count !== 1 ? 's' : '') + ' configured'; } } function setupFormHandlers() { var createUserForm = document.querySelector('#createUserModal form'); if (createUserForm) { createUserForm.addEventListener('submit', function(e) { e.preventDefault(); window.UICore.submitFormAjax(createUserForm, { successMessage: 'User created', onSuccess: function(data) { var modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal')); if (modal) modal.hide(); createUserForm.reset(); var existingAlert = document.querySelector('.alert.alert-info.border-0.shadow-sm'); if (existingAlert) existingAlert.remove(); if (data.secret_key) { var alertHtml = ''; var container = document.querySelector('.page-header'); if (container) { container.insertAdjacentHTML('afterend', alertHtml); document.getElementById('copyNewUserAccessKey').addEventListener('click', async function() { await window.UICore.copyToClipboard(data.access_key, this, 'Copy'); }); document.getElementById('copyNewUserSecret').addEventListener('click', async function() { await window.UICore.copyToClipboard(data.secret_key, this, 'Copy'); }); } } var usersGrid = document.querySelector('.row.g-3'); var emptyState = document.querySelector('.empty-state'); if (emptyState) { var emptyCol = emptyState.closest('.col-12'); if (emptyCol) emptyCol.remove(); if (!usersGrid) { var cardBody = document.querySelector('.card-body.px-4.pb-4'); if (cardBody) { cardBody.innerHTML = '
'; usersGrid = cardBody.querySelector('.row.g-3'); } } } if (usersGrid) { var cardHtml = createUserCardHtml(data.access_key, data.display_name, data.policies); usersGrid.insertAdjacentHTML('beforeend', cardHtml); var newCard = usersGrid.lastElementChild; attachUserCardHandlers(newCard, data.access_key, data.display_name); users.push({ access_key: data.access_key, display_name: data.display_name, policies: data.policies || [] }); updateUserCount(); } } }); }); } var policyEditorForm = document.getElementById('policyEditorForm'); if (policyEditorForm) { policyEditorForm.addEventListener('submit', function(e) { e.preventDefault(); var userInputEl = document.getElementById('policyEditorUser'); var key = userInputEl.value; if (!key) return; var template = policyEditorForm.dataset.actionTemplate; policyEditorForm.action = template.replace('ACCESS_KEY_PLACEHOLDER', key); window.UICore.submitFormAjax(policyEditorForm, { successMessage: 'Policies updated', onSuccess: function(data) { policyModal.hide(); var userCard = document.querySelector('[data-access-key="' + key + '"]'); if (userCard) { var cardEl = userCard.closest('.iam-user-card'); var badgeContainer = cardEl ? cardEl.querySelector('[data-policy-badges]') : null; if (badgeContainer && data.policies) { var badges = data.policies.map(function(p) { var bl = getBucketLabel(p.bucket); var pl = getPermissionLevel(p.actions); return '' + '' + '' + '' + window.UICore.escapeHtml(bl) + ' · ' + window.UICore.escapeHtml(pl) + ''; }).join(''); badgeContainer.innerHTML = badges || 'No policies'; } if (cardEl) { var nowAdmin = isAdminUser(data.policies); cardEl.classList.toggle('iam-admin-card', nowAdmin); var roleBadgeEl = cardEl.querySelector('[data-role-badge]'); if (roleBadgeEl) { if (nowAdmin) { roleBadgeEl.className = 'iam-role-badge iam-role-admin'; roleBadgeEl.textContent = 'Admin'; } else { roleBadgeEl.className = 'iam-role-badge iam-role-user'; roleBadgeEl.textContent = 'User'; } } } } var userIndex = users.findIndex(function(u) { return u.access_key === key; }); if (userIndex >= 0 && data.policies) { users[userIndex].policies = data.policies; } } }); }); } var editUserForm = document.getElementById('editUserForm'); if (editUserForm) { editUserForm.addEventListener('submit', function(e) { e.preventDefault(); var key = currentEditKey; window.UICore.submitFormAjax(editUserForm, { successMessage: 'User updated', onSuccess: function(data) { editUserModal.hide(); var newName = data.display_name || document.getElementById('editUserDisplayName').value; var editBtn = document.querySelector('[data-edit-user="' + key + '"]'); if (editBtn) { editBtn.setAttribute('data-display-name', newName); var card = editBtn.closest('.iam-user-card'); if (card) { var nameEl = card.querySelector('h6'); if (nameEl) { nameEl.textContent = newName; nameEl.title = newName; } var itemWrapper = card.closest('.iam-user-item'); if (itemWrapper) { itemWrapper.setAttribute('data-display-name', newName.toLowerCase()); } } } var userIndex = users.findIndex(function(u) { return u.access_key === key; }); if (userIndex >= 0) { users[userIndex].display_name = newName; } if (key === currentUserKey) { document.querySelectorAll('.sidebar-user .user-name').forEach(function(el) { var truncated = newName.length > 16 ? newName.substring(0, 16) + '...' : newName; el.textContent = truncated; el.title = newName; }); document.querySelectorAll('.sidebar-user[data-username]').forEach(function(el) { el.setAttribute('data-username', newName); }); } } }); }); } var deleteUserForm = document.getElementById('deleteUserForm'); if (deleteUserForm) { deleteUserForm.addEventListener('submit', function(e) { e.preventDefault(); var key = currentDeleteKey; window.UICore.submitFormAjax(deleteUserForm, { successMessage: 'User deleted', onSuccess: function(data) { deleteUserModal.hide(); if (key === currentUserKey) { window.location.href = '/ui/'; return; } var deleteBtn = document.querySelector('[data-delete-user="' + key + '"]'); if (deleteBtn) { var cardCol = deleteBtn.closest('[class*="col-"]'); if (cardCol) { cardCol.remove(); } } users = users.filter(function(u) { return u.access_key !== key; }); updateUserCount(); } }); }); } } function setupSearch() { var searchInput = document.getElementById('iam-user-search'); if (!searchInput) return; searchInput.addEventListener('input', function() { var query = searchInput.value.toLowerCase().trim(); var items = document.querySelectorAll('.iam-user-item'); var noResults = document.getElementById('iam-no-results'); var visibleCount = 0; items.forEach(function(item) { var name = item.getAttribute('data-display-name') || ''; var key = item.getAttribute('data-access-key-filter') || ''; var matches = !query || name.indexOf(query) >= 0 || key.indexOf(query) >= 0; item.classList.toggle('d-none', !matches); if (matches) visibleCount++; }); if (noResults) { noResults.classList.toggle('d-none', visibleCount > 0); } }); } function copyAccessKey(btn) { var key = btn.getAttribute('data-copy-access-key'); if (!key) return; var originalHtml = btn.innerHTML; navigator.clipboard.writeText(key).then(function() { btn.innerHTML = ''; btn.style.color = '#22c55e'; setTimeout(function() { btn.innerHTML = originalHtml; btn.style.color = ''; }, 1200); }).catch(function() {}); } function setupCopyAccessKeyButtons() { document.querySelectorAll('[data-copy-access-key]').forEach(function(btn) { btn.addEventListener('click', function() { copyAccessKey(btn); }); }); } return { init: init }; })();