let rbacState = { users: [], roles: [], permissions: {}, assignments: [], selectedUserId: '', mainView: 'users', activeTab: 'roles', userSearch: '', roleSearch: '', permissionSearch: '', rolePermissionSelection: new Set(), resourceOptions: [], selectedResourceIds: new Set(), selectedResourceMeta: new Map(), resourceSearchTimer: null, resourceRequestSeq: 0, resourcePage: 0, resourcePageSize: 12, resourceHasMore: false, assignmentSearch: '', assignmentType: 'all', assignmentPage: 0, assignmentPageSize: 8, assignmentResourceType: 'conversation', showEffectivePermissions: false, auditLogs: [], auditLoading: false, pendingRoleUserId: '', pendingUserRoles: new Set(), editingRoleIsSystem: false, savingRoleId: '', }; const rbacScopeMeta = { all: { tone: 'is-warn' }, assigned: { tone: 'is-info' }, own: { tone: '' }, }; const rbacResourceLabels = { project: '项目', conversation: '对话', vulnerability: '漏洞', webshell: 'WebShell 连接', batch_task: '批量任务', c2_listener: 'C2 监听器', }; function rbacText(value, fallback = '') { return value == null || value === '' ? fallback : String(value); } function rbacEscape(value) { if (typeof escapeHtml === 'function') return escapeHtml(rbacText(value)); const div = document.createElement('div'); div.textContent = rbacText(value); return div.innerHTML; } function rbacT(key, fallback) { const options = arguments.length > 2 ? arguments[2] : undefined; const translator = typeof window !== 'undefined' && typeof window.t === 'function' ? window.t : null; if (translator) { const translated = translator(key, options); if (translated && translated !== key) return translated; } return fallback; } function rbacPermissionLabel(key) { const parts = String(key || '').split(':'); return rbacT(`rbac.permissionDescriptions.${parts[0] || 'other'}.${parts[1] || 'unknown'}`, rbacState.permissions[key] || key); } function rbacPermissionModuleLabel(module) { return rbacT(`rbac.permissionModules.${module}`, module); } function rbacResourceLabel(type) { return rbacT(`rbac.resourceTypes.${type}`, rbacResourceLabels[type] || type); } function rbacRoleName(role) { if (!role) return ''; return rbacRoleIsSystem(role) ? rbacT(`rbac.systemRoles.${role.id}.name`, role.name) : role.name; } function rbacRoleDescription(role) { if (!role) return ''; return rbacRoleIsSystem(role) ? rbacT(`rbac.systemRoles.${role.id}.description`, role.description || '') : (role.description || ''); } function rbacShortId(value) { const id = rbacText(value); if (id.length <= 16) return id; return `${id.slice(0, 8)}…${id.slice(-4)}`; } function rbacFormatResourceDetail(type, detail) { const text = rbacText(detail); if (!text) return ''; if (type === 'conversation' && text) { return rbacT('rbac.resourceDetailProject', '所属项目 {{id}}', { id: rbacShortId(text) }); } return text; } async function rbacCopyResourceId(id) { const value = rbacText(id); if (!value) return; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(value); } else { const input = document.createElement('textarea'); input.value = value; document.body.appendChild(input); input.select(); document.execCommand('copy'); document.body.removeChild(input); } rbacNotify(rbacT('rbac.copiedId', '已复制'), 'success'); } catch (error) { rbacNotify(`${rbacT('rbac.copyId', '复制 ID')}: ${error.message || error}`, 'error'); } } function rbacPendingSelectionCount() { const manualIds = document.getElementById('rbac-assignment-id')?.value.split(/[\s,,;;]+/).filter(Boolean) || []; return new Set([...rbacState.selectedResourceIds, ...manualIds]).size; } function rbacRememberSelectedResource(resource) { if (!resource || !resource.id) return; rbacState.selectedResourceMeta.set(resource.id, { label: resource.label || resource.id, detail: resource.detail || '', type: document.getElementById('rbac-assignment-type')?.value || '', }); } function rbacSelectedResourceMeta(resourceId) { if (rbacState.selectedResourceMeta.has(resourceId)) { return rbacState.selectedResourceMeta.get(resourceId); } const cached = rbacState.resourceOptions.find(item => item.id === resourceId); if (cached) { return { label: cached.label || cached.id, detail: cached.detail || '', type: document.getElementById('rbac-assignment-type')?.value || '', }; } return { label: resourceId, detail: '', type: document.getElementById('rbac-assignment-type')?.value || '' }; } function clearRbacResourceSelection() { rbacState.selectedResourceIds.clear(); rbacState.selectedResourceMeta.clear(); const manual = document.getElementById('rbac-assignment-id'); if (manual) manual.value = ''; renderRbacResourcePicker(); syncRbacAssignmentSubmit(); } function renderRbacNoRoleNotice(user) { const box = document.getElementById('rbac-no-role-notice'); if (!box) return; const hasRoles = user && rbacUserRoles(user).length > 0; if (!user || hasRoles) { box.hidden = true; box.innerHTML = ''; return; } box.hidden = false; box.innerHTML = `
${rbacEscape(rbacT('rbac.noRoleNoticeTitle', '尚未分配角色'))} ${rbacEscape(rbacT('rbac.noRoleNoticeHint', '请先分配角色,再配置资源授权。无角色时仅可访问本人创建的数据,且大部分功能不可用。'))}
`; } function renderRbacPendingSelection() { const bar = document.getElementById('rbac-pending-bar'); const list = document.getElementById('rbac-pending-list'); const count = document.getElementById('rbac-pending-count'); const manualIds = document.getElementById('rbac-assignment-id')?.value.split(/[\s,,;;]+/).map(id => id.trim()).filter(Boolean) || []; const selectedIds = Array.from(rbacState.selectedResourceIds); const total = new Set([...selectedIds, ...manualIds]).size; if (count) count.textContent = String(total); if (!bar || !list) return; if (total === 0) { bar.hidden = true; list.innerHTML = ''; return; } bar.hidden = false; const resourceType = document.getElementById('rbac-assignment-type')?.value || ''; const items = [ ...selectedIds.map(id => ({ id, meta: rbacSelectedResourceMeta(id), manual: false })), ...manualIds.filter(id => !rbacState.selectedResourceIds.has(id)).map(id => ({ id, meta: { label: id, detail: rbacT('rbac.advancedAssignment', '高级:直接输入资源 ID'), type: resourceType }, manual: true })), ]; list.innerHTML = items.map(item => `
${rbacEscape(item.meta.label || item.id)} ${rbacEscape(rbacShortId(item.id))}
${item.manual ? '' : ``}
`).join(''); } function rbacNotify(message, type = 'info') { if (typeof showNotification === 'function') showNotification(message, type); else if (type === 'error') alert(message); } function rbacUserDisplayName(user) { return (user && (user.displayName || user.display_name || user.username)) || ''; } function rbacUserIsBuiltin(user) { return !!(user && (user.isBuiltin || user.is_builtin || user.id === 'admin')); } function rbacUserRoles(user) { return Array.isArray(user && user.roles) ? user.roles : []; } function resolveUserEffectiveAccess(user) { const userRoles = user ? rbacUserRoles(user) : []; const effectivePermissions = new Set(); const permissionsByModule = {}; let effectiveScope = 'own'; userRoles.forEach(roleId => { const role = rbacState.roles.find(r => r.id === roleId); (role && role.permissions || []).forEach(permission => { effectivePermissions.add(permission); const module = permission.split(':')[0] || 'other'; if (!permissionsByModule[module]) permissionsByModule[module] = []; permissionsByModule[module].push(permission); }); if (role && role.scope === 'all') effectiveScope = 'all'; else if (role && role.scope === 'assigned' && effectiveScope !== 'all') effectiveScope = 'assigned'; }); const assignmentCount = user ? rbacState.assignments.filter(a => (a.userId || a.user_id) === user.id).length : 0; return { userRoles, effectivePermissions, permissionsByModule, effectiveScope, assignmentCount }; } function toggleRbacEffectivePermissions() { rbacState.showEffectivePermissions = !rbacState.showEffectivePermissions; renderRbacEffectivePermissions(selectedRbacUser()); } function renderRbacEffectivePermissions(user) { const box = document.getElementById('rbac-effective-permissions'); if (!box) return; if (!user || !rbacState.showEffectivePermissions) { box.hidden = true; box.innerHTML = ''; return; } const access = resolveUserEffectiveAccess(user); const modules = Object.keys(access.permissionsByModule).sort(); if (!modules.length) { box.hidden = false; box.innerHTML = `
${rbacT('rbac.empty.noEffectivePermissions', '暂无有效权限')}${rbacEscape(rbacT('rbac.empty.assignRoleFirst', '分配角色后即可查看权限明细'))}
`; return; } box.hidden = false; box.innerHTML = `
${rbacEscape(rbacT('rbac.effectivePermissionsDetail', '有效权限明细'))}
${modules.map(module => `

${rbacEscape(rbacPermissionModuleLabel(module))}${access.permissionsByModule[module].length}

${access.permissionsByModule[module].sort().map(key => `${rbacEscape(rbacPermissionLabel(key))}`).join('')}
`).join('')}
`; } function rbacRoleIsSystem(role) { return !!(role && (role.isSystem || role.is_system)); } async function rbacRun(action, fallbackMessage) { try { await action(); return true; } catch (error) { rbacNotify(`${fallbackMessage}: ${error.message || error}`, 'error'); return false; } } async function initPlatformRbacPage() { if (typeof window !== 'undefined' && window.i18nReady) await window.i18nReady; const typeInput = document.getElementById('rbac-assignment-type'); if (typeInput) rbacState.assignmentResourceType = typeInput.value || 'conversation'; await rbacRun(loadPlatformRbac, rbacT('rbac.errors.loadFailed', '加载失败')); initRbacSelects(); } function initRbacSelects() { if (typeof window.initSettingsCustomSelects !== 'function') return; ['page-platform-rbac', 'rbac-user-modal', 'rbac-role-modal'].forEach(id => { const root = document.getElementById(id); if (root) window.initSettingsCustomSelects(root); }); if (typeof window.refreshSettingsCustomSelects === 'function') { window.refreshSettingsCustomSelects(); } } async function refreshRbacAssignments() { const res = await apiFetch('/api/rbac/resource-assignments'); const result = await res.json().catch(() => ({})); if (!res.ok) throw new Error(result.error || rbacT('rbac.errors.loadDataFailed', '无法加载权限数据')); rbacState.assignments = result.assignments || []; renderRbacOverview(); renderRbacUsers(); renderRbacAssignments(); } async function refreshRbacUserRoles(userId, roles) { const user = rbacState.users.find(item => item.id === userId); if (user) user.roles = roles; rbacState.pendingRoleUserId = ''; ensurePendingUserRoles(selectedRbacUser()); renderRbacOverview(); renderRbacUsers(); renderRbacRoles(); } async function loadPlatformRbac() { const responses = await Promise.all([ apiFetch('/api/rbac/metadata'), apiFetch('/api/rbac/users'), apiFetch('/api/rbac/roles'), apiFetch('/api/rbac/resource-assignments'), ]); const payloads = await Promise.all(responses.map(response => response.json().catch(() => ({})))); const failedIndex = responses.findIndex(response => !response.ok); if (failedIndex >= 0) { throw new Error(payloads[failedIndex].error || rbacT('rbac.errors.loadDataFailed', '无法加载权限数据')); } const [meta, users, roles, assignments] = payloads; rbacState.permissions = meta.permissions || {}; rbacState.users = users.users || []; rbacState.roles = roles.roles || []; rbacState.assignments = assignments.assignments || []; if (!rbacState.selectedUserId && rbacState.users.length) { rbacState.selectedUserId = rbacState.users[0].id; } if (rbacState.selectedUserId && !rbacState.users.some(u => u.id === rbacState.selectedUserId)) { rbacState.selectedUserId = rbacState.users[0] ? rbacState.users[0].id : ''; } renderRbac(); } function selectedRbacUser() { return rbacState.users.find(u => u.id === rbacState.selectedUserId) || null; } function renderRbac() { renderRbacOverview(); renderRbacUsers(); renderRbacRoles(); renderRbacAssignments(); renderRbacRoleCatalog(); switchRbacView(rbacState.mainView); } function switchRbacView(view) { rbacState.mainView = view === 'roles' ? 'roles' : 'users'; document.querySelectorAll('.rbac-workspace-tab').forEach(button => { const active = button.dataset.rbacView === rbacState.mainView; button.classList.toggle('is-active', active); button.setAttribute('aria-selected', String(active)); }); const usersView = document.getElementById('rbac-view-users'); const rolesView = document.getElementById('rbac-view-roles'); if (usersView) usersView.hidden = rbacState.mainView !== 'users'; if (rolesView) rolesView.hidden = rbacState.mainView !== 'roles'; } function renderRbacOverview() { const enabled = rbacState.users.filter(user => user.enabled).length; const setText = (id, value) => { const el = document.getElementById(id); if (el) el.textContent = String(value); }; setText('rbac-metric-users', rbacState.users.length); setText('rbac-metric-enabled', enabled); setText('rbac-metric-roles', rbacState.roles.length); setText('rbac-metric-assignments', rbacState.assignments.length); const enabledMetric = document.querySelector('.rbac-metric-enabled'); if (enabledMetric) { const allEnabled = rbacState.users.length > 0 && enabled === rbacState.users.length; enabledMetric.hidden = allEnabled; } const user = selectedRbacUser(); const context = document.getElementById('rbac-metric-context'); if (context) { if (!user) { context.hidden = true; context.textContent = ''; } else { const access = resolveUserEffectiveAccess(user); context.hidden = false; context.textContent = rbacT('rbac.metricUserContext', '当前成员:{{name}} · {{roles}} 个角色 · {{grants}} 项授权', { name: rbacUserDisplayName(user), roles: access.userRoles.length, grants: access.assignmentCount, }); } } } function renderRbacUsers() { const list = document.getElementById('rbac-users-list'); const count = document.getElementById('rbac-users-count'); if (count) count.textContent = String(rbacState.users.length); if (!list) return; const keyword = rbacState.userSearch.trim().toLowerCase(); const users = keyword ? rbacState.users.filter(user => `${user.username || ''} ${rbacUserDisplayName(user)}`.toLowerCase().includes(keyword)) : rbacState.users; if (!users.length) { list.innerHTML = `
${keyword ? rbacT('rbac.empty.noMatchingUsers', '没有匹配的成员') : rbacT('rbac.empty.noUsers', '还没有平台成员')}${keyword ? rbacT('rbac.empty.tryAnotherKeyword', '试试其他关键词') : rbacT('rbac.empty.addFirstUser', '点击右上角“添加成员”开始配置')}
`; return; } list.innerHTML = users.map(user => { const active = user.id === rbacState.selectedUserId ? ' is-active' : ''; const roleNames = rbacUserRoles(user).map(roleId => { const role = rbacState.roles.find(r => r.id === roleId); return role ? rbacRoleName(role) : roleId; }).join(' / '); const assignmentCount = rbacState.assignments.filter(a => (a.userId || a.user_id) === user.id).length; const summary = `${roleNames || rbacT('rbac.noRoleAssigned', '未分配角色')} · ${assignmentCount ? rbacT('rbac.resourceCount', '{{count}} 项资源', { count: assignmentCount }) : rbacT('rbac.noExplicitGrant', '无单独授权')}`; const handle = `@${user.username}${rbacUserIsBuiltin(user) ? ` · ${rbacT('rbac.builtinAccount', '内置账号')}` : ''}`; return ` `; }).join(''); } function setRbacUserSearch(value) { rbacState.userSearch = value || ''; renderRbacUsers(); } function selectRbacUser(userId) { if (userId !== rbacState.selectedUserId && selectedUserRolesAreDirty() && !confirm(rbacT('rbac.confirmDiscardSwitchUser', '当前角色变更尚未保存,确定放弃并切换成员吗?'))) return; rbacState.selectedUserId = userId; rbacState.pendingRoleUserId = ''; rbacState.pendingUserRoles.clear(); rbacState.selectedResourceIds.clear(); rbacState.selectedResourceMeta.clear(); rbacState.resourcePage = 0; rbacState.assignmentPage = 0; rbacState.showEffectivePermissions = false; renderRbac(); if (rbacState.activeTab === 'assignments') loadRbacResourceOptions(); } function setRbacRoleSearch(value) { rbacState.roleSearch = value || ''; renderRbacRoleCatalog(); } function rbacScopeInfo(scope) { const meta = rbacScopeMeta[scope] || { tone: '' }; return { tone: meta.tone, label: rbacT(`rbac.scopes.${scope}.label`, scope || rbacT('rbac.notConfigured', '未设置')), hint: rbacT(`rbac.scopes.${scope}.hint`, ''), }; } function renderRbacRoleCatalog() { const box = document.getElementById('rbac-role-catalog'); if (!box) return; const query = rbacState.roleSearch.trim().toLowerCase(); const roles = rbacState.roles.filter(role => { const permissionText = (role.permissions || []).join(' '); return !query || `${rbacRoleName(role)} ${rbacRoleDescription(role)} ${permissionText}`.toLowerCase().includes(query); }); if (!roles.length) { box.innerHTML = `
${rbacT('rbac.empty.noMatchingRoles', '没有匹配的角色')}${rbacT('rbac.empty.adjustRoleSearch', '调整搜索条件或新建自定义角色')}
`; return; } box.innerHTML = roles.map(role => { const permissions = role.permissions || []; const modules = Array.from(new Set(permissions.map(key => key.split(':')[0]))).filter(Boolean); const members = rbacState.users.filter(user => rbacUserRoles(user).includes(role.id)).length; const scope = rbacScopeInfo(role.scope); return `

${rbacEscape(rbacRoleName(role))}

${rbacEscape(rbacRoleDescription(role) || rbacT('rbac.noRoleDescription', '暂无角色说明'))}

${rbacRoleIsSystem(role) ? `${rbacT('rbac.systemBuiltin', '系统内置')}` : `${rbacT('rbac.customRole', '自定义')}`}
${permissions.length}${rbacT('rbac.operationPermissions', '操作权限')}
${members}${rbacT('rbac.assignedMembers', '已分配成员')}
${rbacEscape(scope.label)}${rbacT('rbac.scope', '资源范围')}
${modules.slice(0, 6).map(module => `${rbacEscape(rbacPermissionModuleLabel(module))}`).join('')}${modules.length > 6 ? `+${modules.length - 6}` : ''}
${rbacEscape(scope.hint)}
${rbacRoleIsSystem(role) ? '' : ``}
`; }).join(''); } function renderRbacRoles() { const title = document.getElementById('rbac-selected-title'); const subtitle = document.getElementById('rbac-selected-subtitle'); const summary = document.getElementById('rbac-selected-summary'); const editBtn = document.getElementById('rbac-edit-user-btn'); const deleteBtn = document.getElementById('rbac-delete-user-btn'); const user = selectedRbacUser(); ensurePendingUserRoles(user); if (title) { if (user) { const status = user.enabled ? rbacT('rbac.statusEnabled', '启用') : rbacT('rbac.statusDisabled', '停用'); const displayName = rbacUserDisplayName(user); const handle = user.username === displayName ? '' : `@${user.username} · `; title.innerHTML = `${rbacEscape(displayName)}${rbacEscape(handle)}${rbacEscape(status)}`; } else { title.textContent = rbacT('rbac.selectUser', '选择用户'); } } if (subtitle) { subtitle.textContent = user ? '' : rbacT('rbac.selectUserHint', '选择一个平台用户后编辑角色、状态与资源授权。'); subtitle.hidden = !!user; } if (editBtn) editBtn.disabled = !user; if (deleteBtn) deleteBtn.disabled = !user || rbacUserIsBuiltin(user); renderRbacNoRoleNotice(user); if (summary) { if (!user) { summary.innerHTML = ''; } else { const access = resolveUserEffectiveAccess(user); const scope = rbacScopeInfo(access.effectiveScope); const permissionsToggle = access.effectivePermissions.size ? ` ` : ''; summary.innerHTML = `
${rbacT('rbac.roles', '角色')}${access.userRoles.length}
${rbacT('rbac.effectivePermissions', '有效权限')}${access.effectivePermissions.size}${permissionsToggle}
${rbacT('rbac.effectiveScope', '有效资源范围')}${rbacEscape(scope.label)}
${rbacT('rbac.metricAssignments', '资源授权')}${access.assignmentCount}
`; } } renderRbacEffectivePermissions(user); const box = document.getElementById('rbac-roles-list'); if (!box) return; if (!rbacState.roles.length) { box.innerHTML = `
${rbacT('rbac.empty.noRoles', '暂无角色')}
`; return; } const userRoles = user ? rbacState.pendingUserRoles : new Set(); box.innerHTML = rbacState.roles.map(role => { const checked = userRoles.has(role.id); const permissionCount = (role.permissions || []).length; const assignedUsers = rbacState.users.filter(u => rbacUserRoles(u).includes(role.id)).length; const scope = rbacScopeInfo(role.scope); return `
${rbacEscape(scope.label)}

${rbacEscape(rbacRoleDescription(role) || rbacT('rbac.noDescription', '无描述'))}

${rbacT('rbac.permissionCount', '{{count}} 项权限', { count: permissionCount })} ${rbacT('rbac.userCount', '{{count}} 个用户', { count: assignedUsers })} ${rbacEscape(scope.hint)}
`; }).join(''); syncRbacRoleSaveActions(); } function ensurePendingUserRoles(user) { if (!user) { rbacState.pendingRoleUserId = ''; rbacState.pendingUserRoles.clear(); return; } if (rbacState.pendingRoleUserId !== user.id) { rbacState.pendingRoleUserId = user.id; rbacState.pendingUserRoles = new Set(rbacUserRoles(user)); } } function toggleSelectedUserRole(roleId, enabled) { if (!selectedRbacUser()) return; if (enabled) rbacState.pendingUserRoles.add(roleId); else rbacState.pendingUserRoles.delete(roleId); renderRbacRoles(); } function selectedUserRolesAreDirty() { const user = selectedRbacUser(); if (!user) return false; const current = new Set(rbacUserRoles(user)); return current.size !== rbacState.pendingUserRoles.size || Array.from(current).some(roleId => !rbacState.pendingUserRoles.has(roleId)); } function syncRbacRoleSaveActions() { const dirty = selectedUserRolesAreDirty(); const indicator = document.getElementById('rbac-role-unsaved'); const reset = document.getElementById('rbac-role-reset-btn'); const apply = document.getElementById('rbac-role-apply-btn'); if (indicator) indicator.hidden = !dirty; if (reset) reset.disabled = !dirty; if (apply) apply.disabled = !dirty; } function resetSelectedUserRoles() { const user = selectedRbacUser(); rbacState.pendingUserRoles = new Set(rbacUserRoles(user)); renderRbacRoles(); } async function saveSelectedUserRoles() { const user = selectedRbacUser(); if (!user || !selectedUserRolesAreDirty()) return; const roles = Array.from(rbacState.pendingUserRoles); const saved = await rbacRun(async () => { const res = await apiFetch(`/api/rbac/users/${encodeURIComponent(user.id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ display_name: user.displayName || user.display_name || '', enabled: user.enabled, roles, }), }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || rbacT('rbac.errors.saveFailed', '保存失败')); await refreshRbacUserRoles(user.id, roles); rbacNotify(rbacT('rbac.messages.roleChangesSaved', '角色变更已保存,权限立即生效'), 'success'); if (rbacState.activeTab === 'audit') loadRbacUserAuditLogs(); }, rbacT('rbac.errors.saveFailed', '保存失败')); if (!saved) { renderRbacRoles(); } } function switchRbacTab(tab) { if (tab !== rbacState.activeTab && selectedUserRolesAreDirty() && !confirm(rbacT('rbac.confirmDiscardSwitchTab', '当前角色变更尚未保存,确定放弃并切换标签吗?'))) return; if (tab !== rbacState.activeTab && selectedUserRolesAreDirty()) resetSelectedUserRoles(); rbacState.activeTab = tab; document.querySelectorAll('.rbac-tab').forEach(btn => btn.classList.toggle('is-active', btn.dataset.rbacTab === tab)); const roles = document.getElementById('rbac-tab-roles'); const assignments = document.getElementById('rbac-tab-assignments'); const audit = document.getElementById('rbac-tab-audit'); if (roles) roles.hidden = tab !== 'roles'; if (assignments) assignments.hidden = tab !== 'assignments'; if (audit) audit.hidden = tab !== 'audit'; if (tab === 'assignments') loadRbacResourceOptions(); if (tab === 'audit') loadRbacUserAuditLogs(); initRbacSelects(); } function focusRbacRoleAssignment() { switchRbacTab('roles'); requestAnimationFrame(() => { const panel = document.getElementById('rbac-tab-roles'); const list = document.getElementById('rbac-roles-list'); const section = panel?.querySelector('.rbac-section-toolbar') || list; if (panel) panel.scrollTop = 0; if (section) section.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); const highlight = list || panel; if (highlight) { highlight.classList.remove('rbac-focus-pulse'); void highlight.offsetWidth; highlight.classList.add('rbac-focus-pulse'); highlight.addEventListener('animationend', () => highlight.classList.remove('rbac-focus-pulse'), { once: true }); } const firstCheckbox = list?.querySelector('input[type="checkbox"]:not(:disabled)'); if (firstCheckbox) firstCheckbox.focus({ preventScroll: true }); }); } function renderRbacAssignments() { const box = document.getElementById('rbac-assignments-list'); if (!box) return; const user = selectedRbacUser(); const rows = user ? rbacState.assignments.filter(a => a.userId === user.id || a.user_id === user.id) : []; const count = document.getElementById('rbac-current-assignment-count'); if (count) count.textContent = String(rows.length); if (!user) { box.innerHTML = `
${rbacT('rbac.empty.selectUserFirst', '请先选择用户')}
`; renderRbacAssignmentPagination(0, 1); renderRbacPendingSelection(); return; } if (!rows.length) { box.innerHTML = `
${rbacT('rbac.empty.noAssignments', '暂无资源授权')}${rbacT('rbac.empty.addAssignmentHint', '在左侧勾选资源,或使用上方「确认授权」按钮')}
`; renderRbacAssignmentPagination(0, 1); renderRbacPendingSelection(); return; } const query = rbacState.assignmentSearch.trim().toLowerCase(); const filteredRows = rows.filter(row => { const type = row.resourceType || row.resource_type || ''; const id = row.resourceId || row.resource_id || ''; const label = row.resourceLabel || row.resource_label || ''; const detail = row.resourceDetail || row.resource_detail || ''; return (rbacState.assignmentType === 'all' || type === rbacState.assignmentType) && (!query || `${rbacResourceLabel(type)} ${type} ${label} ${detail} ${id}`.toLowerCase().includes(query)); }); const totalPages = Math.max(1, Math.ceil(filteredRows.length / rbacState.assignmentPageSize)); rbacState.assignmentPage = Math.min(rbacState.assignmentPage, totalPages - 1); const start = rbacState.assignmentPage * rbacState.assignmentPageSize; const visibleRows = filteredRows.slice(start, start + rbacState.assignmentPageSize); if (!filteredRows.length) { box.innerHTML = `
${rbacT('rbac.empty.noMatchingAssignments', '没有匹配的授权')}${rbacT('rbac.empty.adjustAssignmentFilter', '调整资源类型或搜索条件')}
`; renderRbacAssignmentPagination(0, 1); renderRbacPendingSelection(); return; } box.innerHTML = visibleRows.map(row => { const type = row.resourceType || row.resource_type; const id = row.resourceId || row.resource_id; const label = row.resourceLabel || row.resource_label || id; const detail = rbacFormatResourceDetail(type, row.resourceDetail || row.resource_detail || ''); return `
${rbacEscape(rbacResourceLabel(type))} ${rbacEscape(label)} ${rbacEscape(rbacShortId(id))} ${detail ? `${rbacEscape(detail)}` : ''}
`; }).join(''); renderRbacAssignmentPagination(filteredRows.length, totalPages); renderRbacPendingSelection(); } function renderRbacAssignmentPagination(total, totalPages) { const pagination = document.getElementById('rbac-assignment-pagination'); if (!pagination) return; const pages = Math.max(1, totalPages || 1); rbacState.assignmentPage = Math.min(rbacState.assignmentPage, pages - 1); const info = pagination.querySelector('[data-rbac-page-info]'); const previous = pagination.querySelector('[data-rbac-page-previous]'); const next = pagination.querySelector('[data-rbac-page-next]'); if (info) { info.textContent = total ? rbacT('rbac.pagination.pageSummary', '第 {{page}} / {{pages}} 页 · 共 {{total}} 项', { page: rbacState.assignmentPage + 1, pages, total, }) : rbacT('rbac.pagination.emptySummary', '共 0 项'); } if (previous) previous.disabled = total === 0 || rbacState.assignmentPage === 0; if (next) next.disabled = total === 0 || rbacState.assignmentPage >= pages - 1; } function filterRbacAssignments(value) { rbacState.assignmentSearch = value || ''; rbacState.assignmentPage = 0; renderRbacAssignments(); } function changeRbacAssignmentFilter(value) { rbacState.assignmentType = value || 'all'; rbacState.assignmentPage = 0; renderRbacAssignments(); } function changeRbacAssignmentPage(delta) { rbacState.assignmentPage = Math.max(0, rbacState.assignmentPage + delta); renderRbacAssignments(); } function changeRbacResourceType() { const typeInput = document.getElementById('rbac-assignment-type'); if (!typeInput) return; const nextType = typeInput.value; const previousType = rbacState.assignmentResourceType || 'conversation'; if (nextType === previousType) return; if (rbacState.selectedResourceIds.size > 0 && !confirm(rbacT('rbac.confirmDiscardResourceType', '切换资源类型将清空当前选择,确定继续吗?'))) { typeInput.value = previousType; initRbacSelects(); return; } rbacState.assignmentResourceType = nextType; rbacState.selectedResourceIds.clear(); rbacState.selectedResourceMeta.clear(); rbacState.resourcePage = 0; const search = document.getElementById('rbac-resource-search'); if (search) search.value = ''; syncRbacAssignmentSubmit(); loadRbacResourceOptions(); } function queueRbacResourceSearch() { clearTimeout(rbacState.resourceSearchTimer); rbacState.resourcePage = 0; rbacState.resourceSearchTimer = setTimeout(loadRbacResourceOptions, 250); } async function loadRbacResourceOptions() { const picker = document.getElementById('rbac-resource-picker'); const typeInput = document.getElementById('rbac-assignment-type'); if (!picker || !typeInput || !selectedRbacUser()) return; const requestSeq = ++rbacState.resourceRequestSeq; const query = document.getElementById('rbac-resource-search')?.value.trim() || ''; picker.innerHTML = `
${rbacT('rbac.loadingResources', '正在加载真实资源…')}
`; syncRbacResourcePagination(true); try { const offset = rbacState.resourcePage * rbacState.resourcePageSize; const res = await apiFetch(`/api/rbac/resources?type=${encodeURIComponent(typeInput.value)}&q=${encodeURIComponent(query)}&limit=${rbacState.resourcePageSize}&offset=${offset}`); const result = await res.json().catch(() => ({})); if (!res.ok) throw new Error(result.error || rbacT('rbac.errors.loadResourcesFailed', '加载资源失败')); if (requestSeq !== rbacState.resourceRequestSeq) return; rbacState.resourceOptions = result.resources || []; rbacState.resourceHasMore = !!result.has_more; renderRbacResourcePicker(); } catch (error) { if (requestSeq !== rbacState.resourceRequestSeq) return; rbacState.resourceHasMore = false; picker.innerHTML = `
${rbacEscape(error.message || rbacT('rbac.errors.loadResourcesFailed', '加载资源失败'))}
`; syncRbacResourcePagination(false); } } function renderRbacResourcePicker() { const picker = document.getElementById('rbac-resource-picker'); if (!picker) return; const user = selectedRbacUser(); const resourceType = document.getElementById('rbac-assignment-type')?.value || ''; const assigned = new Set(rbacState.assignments .filter(row => (row.userId === user?.id || row.user_id === user?.id) && (row.resourceType || row.resource_type) === resourceType) .map(row => row.resourceId || row.resource_id)); if (!rbacState.resourceOptions.length) { picker.innerHTML = `
${rbacT('rbac.empty.noMatchingResources', '没有匹配的真实资源')}
`; syncRbacResourcePagination(false); return; } picker.innerHTML = rbacState.resourceOptions.map(resource => { const alreadyAssigned = assigned.has(resource.id); const checked = rbacState.selectedResourceIds.has(resource.id); const detail = rbacFormatResourceDetail(resourceType, resource.detail || ''); return ``; }).join(''); syncRbacResourcePagination(false); } function syncRbacResourcePagination(loading) { const pagination = document.getElementById('rbac-resource-pagination'); if (!pagination) return; const previous = pagination.querySelector('[data-rbac-page-previous]'); const next = pagination.querySelector('[data-rbac-page-next]'); const info = pagination.querySelector('[data-rbac-page-info]'); if (previous) previous.disabled = loading || rbacState.resourcePage === 0; if (next) next.disabled = loading || !rbacState.resourceHasMore; if (info) info.textContent = rbacT('rbac.pagination.resourcePage', '第 {{page}} 页', { page: rbacState.resourcePage + 1 }); } function changeRbacResourcePage(delta) { const nextPage = rbacState.resourcePage + delta; if (nextPage < 0 || (delta > 0 && !rbacState.resourceHasMore)) return; rbacState.resourcePage = nextPage; loadRbacResourceOptions(); } function toggleRbacResourceSelection(resourceId, checked) { if (checked) { rbacState.selectedResourceIds.add(resourceId); const resource = rbacState.resourceOptions.find(item => item.id === resourceId); rbacRememberSelectedResource(resource || { id: resourceId, label: resourceId, detail: '' }); } else { rbacState.selectedResourceIds.delete(resourceId); rbacState.selectedResourceMeta.delete(resourceId); if (rbacState.resourceOptions.length) renderRbacResourcePicker(); } syncRbacAssignmentSubmit(); } function syncRbacAssignmentSubmit() { renderRbacPendingSelection(); } function rbacAuditActionLabel(log) { const action = log && log.action ? String(log.action) : ''; const key = `rbac.auditActions.${action}`; const translated = rbacT(key, ''); return translated || (log && log.message) || action; } async function loadRbacUserAuditLogs() { const box = document.getElementById('rbac-audit-list'); const user = selectedRbacUser(); if (!box) return; if (!user) { box.innerHTML = `
${rbacT('rbac.empty.selectUserFirst', '请先选择用户')}
`; return; } rbacState.auditLoading = true; box.innerHTML = `
${rbacT('common.loading', '加载中…')}
`; try { const params = new URLSearchParams({ category: 'rbac', q: user.id, page: '1', page_size: '20', }); const res = await apiFetch(`/api/audit/logs?${params.toString()}`); const result = await res.json().catch(() => ({})); if (!res.ok) throw new Error(result.error || rbacT('rbac.errors.loadAuditFailed', '加载审计记录失败')); rbacState.auditLogs = (result.logs || []).filter(log => { const resourceId = log.resourceId || log.resource_id || ''; const detail = log.detail || {}; const detailUserId = detail.user_id || detail.userId || ''; return resourceId === user.id || detailUserId === user.id || String(log.message || '').includes(user.username || ''); }); renderRbacAuditLogs(); } catch (error) { box.innerHTML = `
${rbacEscape(error.message || rbacT('rbac.errors.loadAuditFailed', '加载审计记录失败'))}
`; } finally { rbacState.auditLoading = false; } } function renderRbacAuditLogs() { const box = document.getElementById('rbac-audit-list'); if (!box) return; if (!rbacState.auditLogs.length) { box.innerHTML = `
${rbacT('rbac.empty.noAuditLogs', '暂无变更记录')}${rbacT('rbac.empty.noAuditLogsHint', '该成员的角色与授权变更会显示在这里')}
`; return; } const formatTime = typeof formatAuditTime === 'function' ? formatAuditTime : (iso) => rbacText(iso); box.innerHTML = rbacState.auditLogs.map(log => `
${rbacEscape(rbacAuditActionLabel(log))} ${rbacEscape(log.actor || rbacT('rbac.unknownActor', '未知操作者'))} ${log.resourceType || log.resource_type ? `${rbacEscape(rbacResourceLabel(log.resourceType || log.resource_type))}` : ''} ${log.resourceId || log.resource_id ? `${rbacEscape(rbacShortId(log.resourceId || log.resource_id))}` : ''} ${rbacEscape(formatTime(log.createdAt || log.created_at))}
`).join(''); } function openRbacUserModal(userId = '') { const user = userId ? rbacState.users.find(u => u.id === userId) : null; document.getElementById('rbac-user-id').value = user ? user.id : ''; document.getElementById('rbac-username').value = user ? user.username : ''; document.getElementById('rbac-username').disabled = !!user; document.getElementById('rbac-display-name').value = user ? (user.displayName || user.display_name || '') : ''; document.getElementById('rbac-password').value = ''; document.getElementById('rbac-user-enabled').checked = user ? !!user.enabled : true; const createRoles = document.getElementById('rbac-user-roles-create-section'); const editRoles = document.getElementById('rbac-user-roles-edit-section'); if (createRoles) createRoles.hidden = !!user; if (editRoles) editRoles.hidden = !user; if (user) renderRbacUserRolesReadonly(user); else renderRbacUserRoleCheckboxes(new Set()); initRbacSelects(); openAppModal('rbac-user-modal'); } function renderRbacUserRolesReadonly(user) { const box = document.getElementById('rbac-user-roles-readonly'); if (!box) return; const roles = rbacUserRoles(user).map(roleId => rbacState.roles.find(role => role.id === roleId)).filter(Boolean); if (!roles.length) { box.innerHTML = `
${rbacT('rbac.noRoleAssigned', '未分配角色')}
`; return; } box.innerHTML = roles.map(role => { const scope = rbacScopeInfo(role.scope); return `
${rbacEscape(rbacRoleName(role))}${rbacEscape(scope.label)}
`; }).join(''); } function openRbacRolesFromUserModal() { closeRbacUserModal(); focusRbacRoleAssignment(); } function openRbacUserModalForSelected() { const user = selectedRbacUser(); if (!user) return; if (selectedUserRolesAreDirty() && !confirm(rbacT('rbac.confirmDiscardEditUser', '当前角色变更尚未保存,确定放弃并编辑成员资料吗?'))) return; if (selectedUserRolesAreDirty()) resetSelectedUserRoles(); openRbacUserModal(user.id); } function closeRbacUserModal() { closeAppModal('rbac-user-modal'); } function renderRbacUserRoleCheckboxes(selected) { const box = document.getElementById('rbac-user-role-checkboxes'); if (!box) return; box.innerHTML = rbacState.roles.map(role => { const scope = rbacScopeInfo(role.scope); return ` `; }).join(''); } async function saveRbacUser() { await rbacRun(async () => { const id = document.getElementById('rbac-user-id').value; const isEdit = !!id; const payload = { display_name: document.getElementById('rbac-display-name').value.trim(), password: document.getElementById('rbac-password').value, enabled: document.getElementById('rbac-user-enabled').checked, }; if (!isEdit) { payload.roles = Array.from(document.querySelectorAll('#rbac-user-role-checkboxes input:checked')).map(el => el.value); payload.username = document.getElementById('rbac-username').value.trim(); } let url = '/api/rbac/users'; let method = 'POST'; if (isEdit) { url += '/' + encodeURIComponent(id); method = 'PUT'; if (!payload.password) delete payload.password; } const res = await apiFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || rbacT('rbac.errors.saveFailed', '保存失败')); closeRbacUserModal(); await loadPlatformRbac(); if (isEdit && rbacState.activeTab === 'audit') loadRbacUserAuditLogs(); }, rbacT('rbac.errors.saveUserFailed', '保存用户失败')); } async function deleteSelectedRbacUser() { await rbacRun(async () => { const user = selectedRbacUser(); if (!user) return; if (rbacUserIsBuiltin(user)) { rbacNotify(rbacT('rbac.deleteBuiltinUser', '内置管理员不能删除'), 'error'); return; } const access = resolveUserEffectiveAccess(user); const confirmMessage = rbacT('rbac.deleteUserConfirmDetailed', '确认删除成员 {{name}}(@{{username}})?\n\n将立即吊销其会话,并清除 {{roles}} 个角色绑定与 {{grants}} 项资源授权。', { name: rbacUserDisplayName(user), username: user.username, roles: access.userRoles.length, grants: access.assignmentCount, }); if (!confirm(confirmMessage)) return; const res = await apiFetch(`/api/rbac/users/${encodeURIComponent(user.id)}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || rbacT('rbac.errors.deleteFailed', '删除失败')); rbacState.selectedUserId = ''; await loadPlatformRbac(); }, rbacT('rbac.errors.deleteUserFailed', '删除用户失败')); } function openRbacRoleModal(roleId = '') { const role = roleId ? rbacState.roles.find(r => r.id === roleId) : null; document.getElementById('rbac-role-id').value = role ? role.id : ''; document.getElementById('rbac-role-name').value = role ? rbacRoleName(role) : ''; document.getElementById('rbac-role-description').value = role ? rbacRoleDescription(role) : ''; document.getElementById('rbac-role-scope').value = role ? role.scope : 'assigned'; rbacState.editingRoleIsSystem = rbacRoleIsSystem(role); ['rbac-role-name', 'rbac-role-description', 'rbac-role-scope'].forEach(id => { const field = document.getElementById(id); if (field) field.disabled = rbacState.editingRoleIsSystem; }); const saveButton = document.getElementById('rbac-role-save-btn'); if (saveButton) saveButton.hidden = rbacState.editingRoleIsSystem; document.querySelectorAll('.rbac-permission-tools button').forEach(button => { button.disabled = rbacState.editingRoleIsSystem; }); const title = document.getElementById('rbac-role-modal-title'); if (title) title.textContent = rbacState.editingRoleIsSystem ? rbacT('rbac.viewSystemRole', '查看系统角色') : (role ? rbacT('rbac.editRole', '编辑角色') : rbacT('rbac.createRole', '新建角色')); const search = document.getElementById('rbac-permission-search'); if (search) search.value = ''; rbacState.permissionSearch = ''; rbacState.rolePermissionSelection = new Set((role && role.permissions) || []); renderRbacPermissionCheckboxes(); initRbacSelects(); openAppModal('rbac-role-modal'); } function closeRbacRoleModal() { closeAppModal('rbac-role-modal'); } function renderRbacPermissionCheckboxes() { const box = document.getElementById('rbac-permission-checkboxes'); if (!box) return; const grouped = {}; const query = rbacState.permissionSearch.trim().toLowerCase(); Object.keys(rbacState.permissions).sort().forEach(key => { const desc = rbacPermissionLabel(key); if (query && !`${key} ${desc}`.toLowerCase().includes(query)) return; const module = key.split(':')[0] || 'other'; if (!grouped[module]) grouped[module] = []; grouped[module].push(key); }); if (!Object.keys(grouped).length) { box.innerHTML = `
${rbacT('rbac.empty.noMatchingPermissions', '没有匹配的权限')}
`; return; } box.innerHTML = Object.keys(grouped).sort().map(module => `

${rbacEscape(rbacPermissionModuleLabel(module))}${rbacEscape(module)}

${grouped[module].length}
${grouped[module].map(key => ` `).join('')}
`).join(''); } function setRbacPermissionSelected(key, checked) { if (checked) rbacState.rolePermissionSelection.add(key); else rbacState.rolePermissionSelection.delete(key); } function filterRbacPermissions(value) { rbacState.permissionSearch = value || ''; renderRbacPermissionCheckboxes(); } function selectVisibleRbacPermissions(checked) { document.querySelectorAll('#rbac-permission-checkboxes input[type="checkbox"]').forEach(input => { input.checked = checked; setRbacPermissionSelected(input.value, checked); }); } async function saveRbacRole() { await rbacRun(async () => { const id = document.getElementById('rbac-role-id').value; const payload = { name: document.getElementById('rbac-role-name').value.trim(), description: document.getElementById('rbac-role-description').value.trim(), scope: document.getElementById('rbac-role-scope').value, permissions: Array.from(rbacState.rolePermissionSelection), }; const url = id ? `/api/rbac/roles/${encodeURIComponent(id)}` : '/api/rbac/roles'; const res = await apiFetch(url, { method: id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || rbacT('rbac.errors.saveFailed', '保存失败')); closeRbacRoleModal(); await loadPlatformRbac(); }, rbacT('rbac.errors.saveRoleFailed', '保存角色失败')); } async function deleteRbacRole(roleId) { await rbacRun(async () => { if (!confirm(rbacT('rbac.deleteRoleConfirm', '确认删除该平台角色?'))) return; const res = await apiFetch(`/api/rbac/roles/${encodeURIComponent(roleId)}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || rbacT('rbac.errors.deleteFailed', '删除失败')); await loadPlatformRbac(); }, rbacT('rbac.errors.deleteRoleFailed', '删除角色失败')); } async function createRbacAssignment() { await rbacRun(async () => { const user = selectedRbacUser(); if (!user) return; const resourceType = document.getElementById('rbac-assignment-type').value; const manualIds = document.getElementById('rbac-assignment-id').value.split(/[\s,,;;]+/).map(id => id.trim()).filter(Boolean); const resourceIds = Array.from(new Set([...rbacState.selectedResourceIds, ...manualIds])); if (!resourceIds.length) { rbacNotify(rbacT('rbac.errors.enterResourceId', '请输入至少一个资源 ID'), 'error'); return; } if (resourceIds.length > 100) { rbacNotify(rbacT('rbac.errors.tooManyResources', '一次最多授权 100 个资源'), 'error'); return; } const res = await apiFetch('/api/rbac/resource-assignments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: user.id, resource_type: resourceType, resource_ids: resourceIds }), }); const result = await res.json().catch(() => ({})); if (!res.ok) throw new Error(result.error || rbacT('rbac.errors.batchGrantFailed', '批量授权失败')); document.getElementById('rbac-assignment-id').value = ''; rbacState.selectedResourceIds.clear(); rbacState.selectedResourceMeta.clear(); await refreshRbacAssignments(); await loadRbacResourceOptions(); syncRbacAssignmentSubmit(); if (rbacState.activeTab === 'audit') loadRbacUserAuditLogs(); const created = Number(result.created || 0); const skipped = Number(result.skipped || 0); rbacNotify(skipped ? rbacT('rbac.messages.grantPartial', '新增 {{created}} 项授权,{{skipped}} 项已存在或重复', { created, skipped }) : rbacT('rbac.messages.grantSuccess', '已授权 {{count}} 项{{resource}}', { count: created, resource: rbacResourceLabel(resourceType) }), 'success'); }, rbacT('rbac.errors.grantFailed', '授权失败')); } async function deleteRbacAssignment(id) { await rbacRun(async () => { const res = await apiFetch(`/api/rbac/resource-assignments/${encodeURIComponent(id)}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || rbacT('rbac.errors.revokeFailed', '撤销失败')); await refreshRbacAssignments(); if (rbacState.activeTab === 'assignments') await loadRbacResourceOptions(); if (rbacState.activeTab === 'audit') loadRbacUserAuditLogs(); }, rbacT('rbac.errors.revokeFailed', '撤销失败')); } document.addEventListener('languagechange', () => { renderRbac(); if (rbacState.resourceOptions.length) renderRbacResourcePicker(); renderRbacPermissionCheckboxes(); renderRbacEffectivePermissions(selectedRbacUser()); if (rbacState.auditLogs.length) renderRbacAuditLogs(); initRbacSelects(); const roleId = document.getElementById('rbac-role-id')?.value; const role = roleId ? rbacState.roles.find(item => item.id === roleId) : null; if (role && rbacRoleIsSystem(role)) { const name = document.getElementById('rbac-role-name'); const description = document.getElementById('rbac-role-description'); const title = document.getElementById('rbac-role-modal-title'); if (name) name.value = rbacRoleName(role); if (description) description.value = rbacRoleDescription(role); if (title) title.textContent = rbacT('rbac.viewSystemRole', '查看系统角色'); } }); window.openRbacRolesFromUserModal = openRbacRolesFromUserModal; window.focusRbacRoleAssignment = focusRbacRoleAssignment; window.toggleRbacEffectivePermissions = toggleRbacEffectivePermissions; window.loadRbacUserAuditLogs = loadRbacUserAuditLogs; window.clearRbacResourceSelection = clearRbacResourceSelection; window.rbacCopyResourceId = rbacCopyResourceId; window.initPlatformRbacPage = initPlatformRbacPage; window.initRbacSelects = initRbacSelects; window.loadPlatformRbac = loadPlatformRbac; window.selectRbacUser = selectRbacUser; window.switchRbacView = switchRbacView; window.setRbacRoleSearch = setRbacRoleSearch; window.setRbacUserSearch = setRbacUserSearch; window.switchRbacTab = switchRbacTab; window.openRbacUserModal = openRbacUserModal; window.openRbacUserModalForSelected = openRbacUserModalForSelected; window.closeRbacUserModal = closeRbacUserModal; window.saveRbacUser = saveRbacUser; window.deleteSelectedRbacUser = deleteSelectedRbacUser; window.openRbacRoleModal = openRbacRoleModal; window.closeRbacRoleModal = closeRbacRoleModal; window.saveRbacRole = saveRbacRole; window.deleteRbacRole = deleteRbacRole; window.filterRbacPermissions = filterRbacPermissions; window.selectVisibleRbacPermissions = selectVisibleRbacPermissions; window.setRbacPermissionSelected = setRbacPermissionSelected; window.createRbacAssignment = createRbacAssignment; window.deleteRbacAssignment = deleteRbacAssignment; window.changeRbacResourceType = changeRbacResourceType; window.queueRbacResourceSearch = queueRbacResourceSearch; window.loadRbacResourceOptions = loadRbacResourceOptions; window.toggleRbacResourceSelection = toggleRbacResourceSelection; window.changeRbacResourcePage = changeRbacResourcePage; window.filterRbacAssignments = filterRbacAssignments; window.changeRbacAssignmentFilter = changeRbacAssignmentFilter; window.changeRbacAssignmentPage = changeRbacAssignmentPage; window.syncRbacAssignmentSubmit = syncRbacAssignmentSubmit; window.resetSelectedUserRoles = resetSelectedUserRoles; window.saveSelectedUserRoles = saveSelectedUserRoles;