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 = '