diff --git a/static/css/main.css b/static/css/main.css index aed80d1..9b38cd3 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1151,17 +1151,123 @@ html.sidebar-will-collapse .sidebar-user { } .iam-user-card { - border: 1px solid var(--myfsio-card-border); - border-radius: 0.75rem; - transition: box-shadow 0.2s ease, transform 0.2s ease; + position: relative; + border: 1px solid var(--myfsio-card-border) !important; + border-radius: 1rem !important; + overflow: hidden; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.iam-user-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #3b82f6, #8b5cf6); + opacity: 0; + transition: opacity 0.2s ease; } .iam-user-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.12), 0 4px 8px -4px rgba(0, 0, 0, 0.08); + border-color: var(--myfsio-accent) !important; +} + +.iam-user-card:hover::before { + opacity: 1; } [data-theme='dark'] .iam-user-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.4), 0 4px 8px -4px rgba(0, 0, 0, 0.3); +} + +.iam-admin-card::before { + background: linear-gradient(90deg, #f59e0b, #ef4444); +} + +.iam-role-badge { + display: inline-flex; + align-items: center; + padding: 0.25em 0.65em; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.iam-role-admin { + background: rgba(245, 158, 11, 0.15); + color: #d97706; +} + +[data-theme='dark'] .iam-role-admin { + background: rgba(245, 158, 11, 0.25); + color: #fbbf24; +} + +.iam-role-user { + background: rgba(59, 130, 246, 0.12); + color: #2563eb; +} + +[data-theme='dark'] .iam-role-user { + background: rgba(59, 130, 246, 0.2); + color: #60a5fa; +} + +.iam-perm-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.3em 0.6em; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 500; + background: rgba(59, 130, 246, 0.08); + color: var(--myfsio-text); + border: 1px solid rgba(59, 130, 246, 0.15); +} + +[data-theme='dark'] .iam-perm-badge { + background: rgba(59, 130, 246, 0.15); + border-color: rgba(59, 130, 246, 0.25); +} + +.iam-copy-key { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: none; + background: transparent; + color: var(--myfsio-muted); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; + flex-shrink: 0; +} + +.iam-copy-key:hover { + background: var(--myfsio-hover-bg); + color: var(--myfsio-text); +} + +.iam-no-results { + text-align: center; + padding: 2rem 1rem; + color: var(--myfsio-muted); +} + +@media (max-width: 768px) { + .iam-user-card:hover { + transform: none; + } } .user-avatar-lg { diff --git a/static/js/iam-management.js b/static/js/iam-management.js index 8f58133..56710f7 100644 --- a/static/js/iam-management.js +++ b/static/js/iam-management.js @@ -15,12 +15,39 @@ window.IAMManagement = (function() { var currentEditKey = null; var currentDeleteKey = null; + var ALL_S3_ACTIONS = ['list', 'read', 'write', 'delete', 'share', 'policy', 'replication', 'lifecycle', 'cors']; + var policyTemplates = { full: [{ bucket: '*', actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'replication', 'lifecycle', 'cors', 'iam:*'] }], readonly: [{ bucket: '*', actions: ['list', 'read'] }], writer: [{ bucket: '*', actions: ['list', 'read', 'write'] }] }; + 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; @@ -39,6 +66,8 @@ window.IAMManagement = (function() { setupDeleteUserModal(); setupRotateSecretModal(); setupFormHandlers(); + setupSearch(); + setupCopyAccessKeyButtons(); } function initModals() { @@ -243,22 +272,29 @@ window.IAMManagement = (function() { } 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 actionText = p.actions && p.actions.includes('*') ? 'full' : (p.actions ? p.actions.length : 0); - return '' + + var bucketLabel = getBucketLabel(p.bucket); + var permLevel = getPermissionLevel(p.actions); + return '' + '' + '' + - '' + window.UICore.escapeHtml(p.bucket) + - '(' + actionText + ')'; + '' + window.UICore.escapeHtml(bucketLabel) + ' · ' + window.UICore.escapeHtml(permLevel) + ''; }).join(''); } else { policyBadges = 'No policies'; } - return '
' + - '
' + + var esc = window.UICore.escapeHtml; + return '
' + + '
' + '
' + '
' + '
' + @@ -267,8 +303,18 @@ window.IAMManagement = (function() { '' + '
' + '
' + - '
' + window.UICore.escapeHtml(displayName) + '
' + - '' + window.UICore.escapeHtml(accessKey) + '' + + '
' + + '
' + esc(displayName) + '
' + + roleBadge + + '
' + + '
' + + '' + esc(accessKey) + '' + + '' + + '
' + '
' + '
' + '
' + '
Bucket Permissions
' + - '
' + policyBadges + '
' + - '
' + + '' + '
'; } @@ -342,6 +388,13 @@ window.IAMManagement = (function() { policyModal.show(); }); } + + var copyBtn = cardElement.querySelector('[data-copy-access-key]'); + if (copyBtn) { + copyBtn.addEventListener('click', function() { + copyAccessKey(copyBtn); + }); + } } function updateUserCount() { @@ -442,17 +495,33 @@ window.IAMManagement = (function() { var userCard = document.querySelector('[data-access-key="' + key + '"]'); if (userCard) { - var badgeContainer = userCard.closest('.iam-user-card').querySelector('.d-flex.flex-wrap.gap-1'); + 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) { - return '' + + var bl = getBucketLabel(p.bucket); + var pl = getPermissionLevel(p.actions); + return '' + '' + '' + - '' + window.UICore.escapeHtml(p.bucket) + - '(' + (p.actions.includes('*') ? 'full' : p.actions.length) + ')'; + '' + 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; }); @@ -485,6 +554,10 @@ window.IAMManagement = (function() { nameEl.textContent = newName; nameEl.title = newName; } + var itemWrapper = card.closest('.iam-user-item'); + if (itemWrapper) { + itemWrapper.setAttribute('data-display-name', newName.toLowerCase()); + } } } @@ -539,6 +612,52 @@ window.IAMManagement = (function() { } } + 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 }; diff --git a/templates/iam.html b/templates/iam.html index 85c13fe..de01db9 100644 --- a/templates/iam.html +++ b/templates/iam.html @@ -110,10 +110,26 @@ {% else %}
{% if users %} + {% if users|length > 1 %} +
+
+ + + + +
+
+ {% endif %}
{% for user in users %} -
-
+ {% set ns = namespace(is_admin=false) %} + {% for policy in user.policies %} + {% if 'iam:*' in policy.actions or '*' in policy.actions %} + {% set ns.is_admin = true %} + {% endif %} + {% endfor %} +
+
@@ -123,8 +139,23 @@
-
{{ user.display_name }}
- {{ user.access_key }} +
+
{{ user.display_name }}
+ {% if ns.is_admin %} + Admin + {% else %} + User + {% endif %} +
+
+ {{ user.access_key }} + +
Bucket Permissions
-
+
{% for policy in user.policies %} - + {% set bucket_label = 'All Buckets' if policy.bucket == '*' else policy.bucket %} + {% if '*' in policy.actions %} + {% set perm_label = 'Full Access' %} + {% elif policy.actions|length >= 9 %} + {% set perm_label = 'Full Access' %} + {% elif 'list' in policy.actions and 'read' in policy.actions and 'write' in policy.actions and 'delete' in policy.actions %} + {% set perm_label = 'Read + Write + Delete' %} + {% elif 'list' in policy.actions and 'read' in policy.actions and 'write' in policy.actions %} + {% set perm_label = 'Read + Write' %} + {% elif 'list' in policy.actions and 'read' in policy.actions %} + {% set perm_label = 'Read Only' %} + {% else %} + {% set perm_label = 'Custom (' ~ policy.actions|length ~ ')' %} + {% endif %} + - {{ policy.bucket }} - {% if '*' in policy.actions %} - (full) - {% else %} - ({{ policy.actions|length }}) - {% endif %} + {{ bucket_label }} ยท {{ perm_label }} {% else %} No policies @@ -196,6 +236,12 @@
{% endfor %}
+
+ + + +

No users match your filter.

+
{% else %}