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 <noreply@anthropic.com>
This commit is contained in:
+30
@@ -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
|
||||
# ============================================================
|
||||
|
||||
+103
@@ -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 = '<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>';
|
||||
return;
|
||||
}
|
||||
umDebounce = setTimeout(() => _umSearchUser(q), 320);
|
||||
});
|
||||
|
||||
async function _umSearchUser(q) {
|
||||
resultsDiv.innerHTML = '<div class="gex-hint">Suche…</div>';
|
||||
try {
|
||||
const data = await api(`/api/users?q=${encodeURIComponent(q)}`);
|
||||
if (!data.items || data.items.length === 0) {
|
||||
resultsDiv.innerHTML = '<div class="gex-hint">Keine Benutzer gefunden.</div>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
for (const u of data.items) {
|
||||
const upn = u.UserPrincipalName || u.Mail || u.Id;
|
||||
html += `<div class="gex-result-item" data-uid="${escapeHtml(u.Id)}"
|
||||
data-name="${escapeHtml(u.DisplayName || upn)}"
|
||||
data-upn="${escapeHtml(upn)}">
|
||||
<span class="gex-user-name">${escapeHtml(u.DisplayName || '—')}</span>
|
||||
<span class="gex-user-upn">${escapeHtml(upn)}</span>
|
||||
</div>`;
|
||||
}
|
||||
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 = `<div class="gex-hint error">Fehler: ${escapeHtml(String(e))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function _umSelectUser(user) {
|
||||
umSelected = user;
|
||||
searchInput.value = user.DisplayName || user.UserPrincipalName;
|
||||
resultsDiv.innerHTML = '';
|
||||
groupsList.innerHTML = '<div class="gex-hint">Lade Gruppen…</div>';
|
||||
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 = '<div class="gex-hint">Keine Gruppen gefunden.</div>';
|
||||
filterWrap.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
_umRenderGroups(data.groups);
|
||||
} catch (e) {
|
||||
groupsList.innerHTML = `<div class="gex-hint error">Fehler: ${escapeHtml(String(e))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function _umRenderGroups(groups) {
|
||||
let html = '';
|
||||
for (const g of groups) {
|
||||
const desc = g.Description ? `<span class="gex-user-upn">${escapeHtml(g.Description)}</span>` : '';
|
||||
const badge = `<span class="gex-type-badge">${escapeHtml(g.Type)}</span>`;
|
||||
html += `<div class="gex-group-row" data-search="${escapeHtml((g.DisplayName || '').toLowerCase())} ${escapeHtml((g.Description || '').toLowerCase())}">
|
||||
<span class="gex-user-name">${escapeHtml(g.DisplayName || '—')}</span>
|
||||
${desc}
|
||||
${badge}
|
||||
</div>`;
|
||||
}
|
||||
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;
|
||||
|
||||
+22
-1
@@ -291,12 +291,13 @@
|
||||
<button class="gex-tab active" data-tab="export">Mitglieder & Export</button>
|
||||
<button class="gex-tab gex-add-pane" data-tab="add">Benutzer hinzufügen</button>
|
||||
<button class="gex-tab gex-add-pane" data-tab="import">CSV-Import</button>
|
||||
<button class="gex-tab" data-tab="user">Benutzer</button>
|
||||
</div>
|
||||
|
||||
<div class="gex-columns group-mgmt-columns">
|
||||
|
||||
<!-- Linke Spalte: Gruppensuche -->
|
||||
<div class="gex-left">
|
||||
<div class="gex-left" id="gexLeft">
|
||||
<div class="gex-section-title">Gruppe suchen</div>
|
||||
<div class="search-wrap" style="margin-bottom:10px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
@@ -399,6 +400,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer-Tab: User → Gruppen -->
|
||||
<div class="gex-right hidden" id="gexPaneUser">
|
||||
<div class="gex-section-title">Benutzer suchen</div>
|
||||
<div class="search-wrap" style="margin-bottom:10px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="search" id="gexUmSearch" class="input input-search" placeholder="Name, UPN oder E-Mail…" autocomplete="off">
|
||||
</div>
|
||||
<div id="gexUmResults" class="gex-results" style="max-height:160px;overflow-y:auto;margin-bottom:12px;">
|
||||
<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>
|
||||
</div>
|
||||
<div class="gex-section-title" id="gexUmGroupsTitle" style="display:none;">
|
||||
Mitglied in <span id="gexUmGroupCount" class="count-badge">0</span> Gruppen
|
||||
</div>
|
||||
<div class="search-wrap" id="gexUmFilterWrap" style="display:none;margin-bottom:8px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="search" id="gexUmFilter" class="input input-search" placeholder="Gruppen filtern…" autocomplete="off">
|
||||
</div>
|
||||
<div id="gexUmGroupsList" class="gex-members-area" style="flex:1;overflow-y:auto;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user