From ec130031cb85ae37383c138395ba2e2f1424d5cc Mon Sep 17 00:00:00 2001 From: Marco Wende Date: Wed, 12 Aug 2026 10:03:49 +0200 Subject: [PATCH] Group Management: Benutzer-Tab mit Gruppen-Mitgliedschaften Neuer Tab "Benutzer" im Group Management: Benutzer suchen und alle Gruppen anzeigen, in denen dieser Mitglied ist (transitiveMemberOf). Gruppen filterbar, mit Typ-Badge (Sicherheitsgruppe/M365/Verteiler). Co-Authored-By: Claude Sonnet 4.6 --- src/Api.ps1 | 30 ++++++++++++++ www/app.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ www/index.html | 23 ++++++++++- www/styles.css | 24 ++++++++++++ 4 files changed, 179 insertions(+), 1 deletion(-) diff --git a/src/Api.ps1 b/src/Api.ps1 index 5697cf1..5873ad3 100644 --- a/src/Api.ps1 +++ b/src/Api.ps1 @@ -66,6 +66,10 @@ function Invoke-ApiHandler { return Remove-GroupMemberEndpoint -GroupId $matches[1] -UserId $matches[2] } + if ($Method -eq "GET" -and $Path -match "^/api/users/([^/]+)/memberof$") { + return Get-UserMemberOfEndpoint -UserId $matches[1] + } + if ($Method -eq "GET" -and $Path -match "^/api/apps/([^/]+)/details$") { return Get-AppDetailsEndpoint -AppId $matches[1] } @@ -1490,6 +1494,32 @@ function Search-UsersEndpoint { return @{ items = $items; count = $items.Count } } +function Get-UserMemberOfEndpoint { + param($UserId) + $err = Test-Connected + if ($err) { return $err } + + $groups = @() + $uri = "https://graph.microsoft.com/v1.0/users/$UserId/transitiveMemberOf/microsoft.graph.group?`$select=id,displayName,description,groupTypes,mailEnabled,securityEnabled&`$top=100" + while ($uri) { + $resp = Invoke-MgGraphRequestRetry -Uri $uri -Method GET + foreach ($g in $resp.value) { + $type = 'Sicherheitsgruppe' + if ($g.groupTypes -contains 'Unified') { $type = 'Microsoft 365' } + elseif ($g.mailEnabled -and -not $g.securityEnabled) { $type = 'Verteiler' } + $groups += [pscustomobject]@{ + Id = $g.id + DisplayName = $g.displayName + Description = $g.description + Type = $type + } + } + $uri = $resp.'@odata.nextLink' + } + $groups = $groups | Sort-Object DisplayName + return @{ groups = $groups; count = $groups.Count } +} + # ============================================================ # Apps # ============================================================ diff --git a/www/app.js b/www/app.js index 6c5ba60..44dc220 100644 --- a/www/app.js +++ b/www/app.js @@ -4372,10 +4372,13 @@ function _gexSwitchTab(tab) { document.getElementById('gexPaneExport').classList.toggle('hidden', tab !== 'export'); document.getElementById('gexPaneAdd').classList.toggle('hidden', tab !== 'add'); document.getElementById('gexPaneImport').classList.toggle('hidden', tab !== 'import'); + document.getElementById('gexPaneUser').classList.toggle('hidden', tab !== 'user'); + document.getElementById('gexLeft').classList.toggle('hidden', tab === 'user'); document.getElementById('gexExportBtn').classList.toggle('hidden', tab !== 'export'); document.getElementById('gexAddBtn').classList.toggle('hidden', tab !== 'add'); document.getElementById('gexStats').textContent = ''; if (tab === 'add') setTimeout(() => document.getElementById('gexUserSearch').focus(), 80); + if (tab === 'user') setTimeout(() => document.getElementById('gexUmSearch').focus(), 80); } document.querySelectorAll('.gex-tab').forEach(btn => { @@ -4603,6 +4606,106 @@ document.getElementById('gexRemoveBtn')?.addEventListener('click', async () => { } }); +// ---- Benutzer → Gruppen (User-Tab) ---- +(function () { + let umDebounce = null; + let umSelected = null; // { Id, DisplayName, UserPrincipalName } + + const searchInput = document.getElementById('gexUmSearch'); + const resultsDiv = document.getElementById('gexUmResults'); + const groupsList = document.getElementById('gexUmGroupsList'); + const groupsTitle = document.getElementById('gexUmGroupsTitle'); + const groupCount = document.getElementById('gexUmGroupCount'); + const filterWrap = document.getElementById('gexUmFilterWrap'); + const filterInput = document.getElementById('gexUmFilter'); + + searchInput.addEventListener('input', () => { + clearTimeout(umDebounce); + const q = searchInput.value.trim(); + if (q.length < 2) { + resultsDiv.innerHTML = '
Mindestens 2 Zeichen eingeben.
'; + return; + } + umDebounce = setTimeout(() => _umSearchUser(q), 320); + }); + + async function _umSearchUser(q) { + resultsDiv.innerHTML = '
Suche…
'; + try { + const data = await api(`/api/users?q=${encodeURIComponent(q)}`); + if (!data.items || data.items.length === 0) { + resultsDiv.innerHTML = '
Keine Benutzer gefunden.
'; + return; + } + let html = ''; + for (const u of data.items) { + const upn = u.UserPrincipalName || u.Mail || u.Id; + html += `
+ ${escapeHtml(u.DisplayName || '—')} + ${escapeHtml(upn)} +
`; + } + resultsDiv.innerHTML = html; + resultsDiv.querySelectorAll('.gex-result-item').forEach(el => { + el.addEventListener('click', () => _umSelectUser({ + Id: el.dataset.uid, + DisplayName: el.dataset.name, + UserPrincipalName: el.dataset.upn + })); + }); + } catch (e) { + resultsDiv.innerHTML = `
Fehler: ${escapeHtml(String(e))}
`; + } + } + + async function _umSelectUser(user) { + umSelected = user; + searchInput.value = user.DisplayName || user.UserPrincipalName; + resultsDiv.innerHTML = ''; + groupsList.innerHTML = '
Lade Gruppen…
'; + groupsTitle.style.display = ''; + filterWrap.style.display = ''; + filterInput.value = ''; + groupCount.textContent = '…'; + + try { + const data = await api(`/api/users/${encodeURIComponent(user.Id)}/memberof`); + groupCount.textContent = data.count; + if (!data.groups || data.groups.length === 0) { + groupsList.innerHTML = '
Keine Gruppen gefunden.
'; + filterWrap.style.display = 'none'; + return; + } + _umRenderGroups(data.groups); + } catch (e) { + groupsList.innerHTML = `
Fehler: ${escapeHtml(String(e))}
`; + } + } + + function _umRenderGroups(groups) { + let html = ''; + for (const g of groups) { + const desc = g.Description ? `${escapeHtml(g.Description)}` : ''; + const badge = `${escapeHtml(g.Type)}`; + html += `
+ ${escapeHtml(g.DisplayName || '—')} + ${desc} + ${badge} +
`; + } + groupsList.innerHTML = html; + } + + filterInput.addEventListener('input', () => { + const q = filterInput.value.toLowerCase(); + groupsList.querySelectorAll('.gex-group-row').forEach(row => { + row.classList.toggle('hidden-filter', q.length > 0 && !row.dataset.search.includes(q)); + }); + }); +})(); + // ---- Benutzer-Suche im Add-Tab ---- let _gexUserSearchTimer = null; diff --git a/www/index.html b/www/index.html index 10b3d98..78f4b98 100644 --- a/www/index.html +++ b/www/index.html @@ -291,12 +291,13 @@ +
-
+
Gruppe suchen
@@ -399,6 +400,26 @@
+ + +
diff --git a/www/styles.css b/www/styles.css index e9fe16b..22dc30a 100644 --- a/www/styles.css +++ b/www/styles.css @@ -4714,6 +4714,30 @@ html[data-theme="dark"] .numInputWrapper span.arrowDown { text-overflow: ellipsis; } +.gex-group-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-bottom: 1px solid var(--hairline-soft); + font-size: 13px; +} +.gex-group-row:last-child { border-bottom: none; } +.gex-group-row.hidden-filter { display: none; } +.gex-group-row .gex-user-name { min-width: 140px; max-width: none; flex: 1; } +.gex-group-row .gex-user-upn { flex: 1; } + +.gex-type-badge { + font-size: 10px; + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; + background: var(--hairline-soft); + color: var(--muted); +} + .gex-loading { display: flex; align-items: center;