From c860ea9a10ddd6ba614c8e76ca3da31813bdc83a Mon Sep 17 00:00:00 2001
From: Marco Wende
Date: Wed, 17 Jun 2026 09:41:43 +0100
Subject: [PATCH] diverse anpassungen
---
Intune/Intune-App-Manager-Web/README.md | 4 +-
Intune/Intune-App-Manager-Web/Run.cmd | 2 +-
Intune/Intune-App-Manager-Web/Start.ps1 | 12 +-
Intune/Intune-App-Manager-Web/src/Api.ps1 | 95 ++++++++++-
Intune/Intune-App-Manager-Web/www/app.js | 159 +++++++++++++++++-
Intune/Intune-App-Manager-Web/www/help.md | 2 +-
Intune/Intune-App-Manager-Web/www/index.html | 81 ++++++---
.../Intune-App-Manager-Web/www/members.html | 2 +-
Intune/Intune-App-Manager-Web/www/styles.css | 113 ++++++++++++-
9 files changed, 429 insertions(+), 41 deletions(-)
diff --git a/Intune/Intune-App-Manager-Web/README.md b/Intune/Intune-App-Manager-Web/README.md
index 199d960..864cea2 100644
--- a/Intune/Intune-App-Manager-Web/README.md
+++ b/Intune/Intune-App-Manager-Web/README.md
@@ -1,6 +1,6 @@
-# Intune App-Manager – Web Edition
+# Intune Manager – Web Edition
-Modernes Web-Frontend fuer den Intune App-Zuweisungs-Manager.
+Modernes Web-Frontend fuer den Intune Manager.
PowerShell startet einen lokalen HTTP-Server, der das Frontend ausliefert
und die Microsoft Graph-Aufrufe ausfuehrt.
diff --git a/Intune/Intune-App-Manager-Web/Run.cmd b/Intune/Intune-App-Manager-Web/Run.cmd
index a65eeca..3e90d21 100644
--- a/Intune/Intune-App-Manager-Web/Run.cmd
+++ b/Intune/Intune-App-Manager-Web/Run.cmd
@@ -16,4 +16,4 @@ REM Run.cmd
REM Run.cmd -Port 8088
REM Run.cmd -NoBrowser
-start "Intune App-Manager" /min powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File "%~dp0Start.ps1" %*
+start "Intune Manager" /min powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File "%~dp0Start.ps1" %*
diff --git a/Intune/Intune-App-Manager-Web/Start.ps1 b/Intune/Intune-App-Manager-Web/Start.ps1
index d0b200b..82390e8 100644
--- a/Intune/Intune-App-Manager-Web/Start.ps1
+++ b/Intune/Intune-App-Manager-Web/Start.ps1
@@ -1,4 +1,4 @@
-# Intune App-Zuweisungs-Manager - Web Edition
+# Intune Manager - Web Edition
# Startet einen lokalen HTTP-Server und oeffnet das moderne Web-Frontend.
#
# Aufruf:
@@ -64,7 +64,7 @@ function Test-RequiredModules {
$nuget = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue
if (-not $nuget -or $nuget.Version -lt [version]"2.8.5.201") {
Write-Host ""
- Write-Host "NuGet-PackageProvider fehlt — wird im User-Scope nachgezogen..." -ForegroundColor Yellow
+ Write-Host "NuGet-PackageProvider fehlt — wird im User-Scope nachgezogen..." -ForegroundColor Yellow
try {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force -ErrorAction Stop | Out-Null
} catch {
@@ -126,7 +126,7 @@ if (-not $SkipModuleCheck) {
)
if (-not (Test-RequiredModules -Required $required -AutoInstall:$AutoInstall)) {
Write-Host ""
- Write-Host "Server-Start abgebrochen — benoetigte Module fehlen." -ForegroundColor Red
+ Write-Host "Server-Start abgebrochen — benoetigte Module fehlen." -ForegroundColor Red
exit 1
}
Write-Host ""
@@ -144,7 +144,7 @@ $script:Settings = Read-Settings
if ($TenantId) { $script:Settings.connection.tenantId = $TenantId }
if ($ClientId) { $script:Settings.connection.clientId = $ClientId }
-# Globale Konfiguration. TenantId/ClientId/Scopes spiegeln die Settings —
+# Globale Konfiguration. TenantId/ClientId/Scopes spiegeln die Settings —
# Connect-Endpoint und Co. lesen weiterhin $script:Config, das nach jedem
# Settings-Save aktualisiert wird.
$script:Config = @{
@@ -177,7 +177,7 @@ $script:BuildStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host ""
Write-Host "==============================================" -ForegroundColor Cyan
-Write-Host " Intune App-Zuweisungs-Manager - Web Edition" -ForegroundColor Cyan
+Write-Host " Intune Manager - Web Edition" -ForegroundColor Cyan
Write-Host " Version: $script:ToolVersion" -ForegroundColor Green
Write-Host " BUILD: $script:BuildStamp" -ForegroundColor DarkGray
Write-Host "==============================================" -ForegroundColor Cyan
@@ -185,7 +185,7 @@ Write-Host ""
# ============================================================
# Alte Instanzen beenden: laeuft das Tool bereits, haelt der alte Prozess den
-# Port (HttpListener via http.sys) — ein zweiter Start scheitert dann still und
+# Port (HttpListener via http.sys) — ein zweiter Start scheitert dann still und
# das alte Fenster bedient weiter mit altem Code. Darum alle anderen Start.ps1-
# Prozesse DIESES Pfads vorher beenden, damit ein Neustart wirklich neu laedt.
# ============================================================
diff --git a/Intune/Intune-App-Manager-Web/src/Api.ps1 b/Intune/Intune-App-Manager-Web/src/Api.ps1
index 6144c15..55aef9f 100644
--- a/Intune/Intune-App-Manager-Web/src/Api.ps1
+++ b/Intune/Intune-App-Manager-Web/src/Api.ps1
@@ -55,6 +55,14 @@ function Invoke-ApiHandler {
return Get-GroupMembersEndpoint -GroupId $matches[1]
}
+ if ($Method -eq "POST" -and $Path -match "^/api/groups/([^/]+)/members$") {
+ return Add-GroupMembersEndpoint -GroupId $matches[1] -Body $Body
+ }
+
+ if ($Method -eq "DELETE" -and $Path -match "^/api/groups/([^/]+)/members/([^/]+)$") {
+ return Remove-GroupMemberEndpoint -GroupId $matches[1] -UserId $matches[2]
+ }
+
if ($Method -eq "GET" -and $Path -match "^/api/apps/([^/]+)/details$") {
return Get-AppDetailsEndpoint -AppId $matches[1]
}
@@ -1191,6 +1199,91 @@ function Get-GroupMembersEndpoint {
return @{ items = $items; count = $items.Count }
}
+function Add-GroupMembersEndpoint {
+ param([string]$GroupId, $Body)
+ $err = Test-Connected
+ if ($err) { return $err }
+
+ if ([string]::IsNullOrWhiteSpace($GroupId)) {
+ return @{ __status = 400; error = "GroupId fehlt" }
+ }
+ if (-not $Body) {
+ return @{ __status = 400; error = "Request-Body fehlt" }
+ }
+
+ $ids = @()
+ if ($Body.userIds) {
+ $ids = @($Body.userIds | ForEach-Object { [string]$_ } | Where-Object { $_ })
+ } elseif ($Body.userId) {
+ $ids = @([string]$Body.userId)
+ }
+ if ($ids.Count -eq 0) {
+ return @{ __status = 400; error = "userId oder userIds fehlt im Body" }
+ }
+
+ $success = 0; $errors = 0
+ $results = @()
+ foreach ($uid in $ids) {
+ try {
+ Add-GraphMember -GroupId $GroupId -DirectoryObjectId $uid
+ $success++
+ $results += @{ userId = $uid; status = "Success" }
+ Write-Host " [ADD MEMBER] GroupId=$GroupId UserId=$uid OK" -ForegroundColor Green
+ } catch {
+ $details = Get-GraphErrorFriendly -ErrorRecord $_
+ if ($details.IsWarning) {
+ $success++
+ $results += @{ userId = $uid; status = "AlreadyMember"; message = $details.Friendly }
+ Write-Host " [ADD MEMBER] $uid bereits Mitglied in $GroupId" -ForegroundColor DarkGreen
+ } else {
+ $errors++
+ $results += @{ userId = $uid; status = "Error"; code = $details.Code; message = $details.Friendly }
+ Write-Host " [ADD MEMBER] FAIL $uid -> $GroupId [$($details.Code)] $($details.Friendly)" -ForegroundColor Red
+ }
+ }
+ }
+
+ if ($script:State.GroupMembers.ContainsKey($GroupId)) {
+ $script:State.GroupMembers.Remove($GroupId)
+ }
+
+ return @{
+ ok = ($errors -eq 0)
+ success = [int]$success
+ errors = [int]$errors
+ total = [int]$ids.Count
+ results = $results
+ }
+}
+
+function Remove-GroupMemberEndpoint {
+ param([string]$GroupId, [string]$UserId)
+ $err = Test-Connected
+ if ($err) { return $err }
+
+ if ([string]::IsNullOrWhiteSpace($GroupId) -or [string]::IsNullOrWhiteSpace($UserId)) {
+ return @{ __status = 400; error = "GroupId und UserId erforderlich" }
+ }
+
+ try {
+ $null = Invoke-MgGraphRequest `
+ -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/members/$UserId/`$ref" `
+ -Method DELETE
+ Write-Host " [REMOVE MEMBER] GroupId=$GroupId UserId=$UserId OK" -ForegroundColor Green
+ } catch {
+ $details = Get-GraphErrorFriendly -ErrorRecord $_
+ $status = if ($details.Code -like '*404*') { 404 } elseif ($details.Code -like '*403*') { 403 } else { 500 }
+ Write-Host " [REMOVE MEMBER] FAIL [$($details.Code)] $($details.Friendly)" -ForegroundColor Red
+ return @{ __status = $status; error = $details.Friendly; code = $details.Code }
+ }
+
+ if ($script:State.GroupMembers.ContainsKey($GroupId)) {
+ $script:State.GroupMembers.Remove($GroupId)
+ }
+
+ return @{ ok = $true; groupId = $GroupId; userId = $UserId }
+}
+
function Search-GroupsEndpoint {
param($Query)
$err = Test-Connected
@@ -2689,7 +2782,7 @@ footer code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;bac
diff --git a/Intune/Intune-App-Manager-Web/www/app.js b/Intune/Intune-App-Manager-Web/www/app.js
index 2f5b0bd..dbb1662 100644
--- a/Intune/Intune-App-Manager-Web/www/app.js
+++ b/Intune/Intune-App-Manager-Web/www/app.js
@@ -1,5 +1,5 @@
// =============================================================
-// Intune App-Manager — Frontend
+// Intune Manager — Frontend
// Workflow: Connect → Select Targets → Pick Apps → Apply
// =============================================================
@@ -4188,6 +4188,9 @@ const GexState = {
selectedGroup: null,
members: null,
memberFilter: '',
+ activeTab: 'export',
+ userSearchResults: [],
+ selectedUsers: new Map(),
};
function openGroupExportModal() {
@@ -4196,6 +4199,9 @@ function openGroupExportModal() {
GexState.selectedGroup = null;
GexState.members = null;
GexState.memberFilter = '';
+ GexState.activeTab = 'export';
+ GexState.userSearchResults = [];
+ GexState.selectedUsers = new Map();
document.getElementById('gexSearch').value = '';
document.getElementById('gexMemberFilter').value = '';
document.getElementById('gexSearchResults').innerHTML = '
Mindestens 2 Zeichen eingeben.
';
@@ -4203,10 +4209,33 @@ function openGroupExportModal() {
document.getElementById('gexMembersTitle').textContent = 'Mitglieder';
document.getElementById('gexStats').textContent = '';
document.getElementById('gexExportBtn').disabled = true;
+ // Add-Tab zurücksetzen
+ document.getElementById('gexUserSearch').value = '';
+ document.getElementById('gexUserResults').innerHTML = '
Mindestens 2 Zeichen eingeben.
';
+ document.getElementById('gexSelectedUsers').innerHTML = '
Noch keine Benutzer ausgewählt.
';
+ document.getElementById('gexSelectedCount').textContent = '0';
+ document.getElementById('gexAddBtn').disabled = true;
+ // Tab-Anzeige auf Export zurücksetzen
+ _gexSwitchTab('export');
document.getElementById('modalGroupExport').classList.remove('hidden');
setTimeout(() => document.getElementById('gexSearch').focus(), 80);
}
+function _gexSwitchTab(tab) {
+ GexState.activeTab = tab;
+ document.querySelectorAll('.gex-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
+ document.getElementById('gexPaneExport').classList.toggle('hidden', tab !== 'export');
+ document.getElementById('gexPaneAdd').classList.toggle('hidden', tab !== 'add');
+ 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);
+}
+
+document.querySelectorAll('.gex-tab').forEach(btn => {
+ btn.addEventListener('click', () => _gexSwitchTab(btn.dataset.tab));
+});
+
document.getElementById('btnGroupExport')?.addEventListener('click', openGroupExportModal);
// Suche mit Debounce
@@ -4257,8 +4286,12 @@ async function _gexSelectGroup(id, name) {
GexState.selectedGroup = { Id: id, DisplayName: name };
GexState.members = null;
GexState.memberFilter = '';
- document.getElementById('gexMemberFilter').value = '';
_gexRenderResults(); // aktualisiert selected-Highlight
+ _gexUpdateAddBtn();
+
+ if (GexState.activeTab !== 'export') return;
+
+ document.getElementById('gexMemberFilter').value = '';
document.getElementById('gexMembersTitle').textContent = `Mitglieder — ${name}`;
document.getElementById('gexStats').textContent = '';
document.getElementById('gexExportBtn').disabled = true;
@@ -4340,6 +4373,128 @@ function _gexRenderMembers() {
if (GexState.memberFilter) _gexApplyFilter();
}
+// ---- Benutzer-Suche im Add-Tab ----
+
+let _gexUserSearchTimer = null;
+document.getElementById('gexUserSearch')?.addEventListener('input', e => {
+ clearTimeout(_gexUserSearchTimer);
+ const q = e.target.value.trim();
+ if (q.length < 2) {
+ GexState.userSearchResults = [];
+ document.getElementById('gexUserResults').innerHTML = '
Mindestens 2 Zeichen eingeben.
';
+ return;
+ }
+ document.getElementById('gexUserResults').innerHTML = '
';
+ _gexUserSearchTimer = setTimeout(() => _gexDoUserSearch(q), 350);
+});
+
+async function _gexDoUserSearch(q) {
+ try {
+ const data = await api(`/api/users?q=${encodeURIComponent(q)}`);
+ GexState.userSearchResults = data.items || [];
+ _gexRenderUserResults();
+ } catch (e) {
+ document.getElementById('gexUserResults').innerHTML = `
Suche fehlgeschlagen: ${escapeHtml(e.message)}
`;
+ }
+}
+
+function _gexRenderUserResults() {
+ const el = document.getElementById('gexUserResults');
+ if (!el) return;
+ const items = GexState.userSearchResults;
+ if (items.length === 0) {
+ el.innerHTML = '
Keine Benutzer gefunden.
';
+ return;
+ }
+ el.innerHTML = items.map(u => {
+ const already = GexState.selectedUsers.has(u.Id);
+ return `
+ ${escapeHtml(u.DisplayName || '—')}
+ ${escapeHtml(u.UserPrincipalName || u.Mail || '')}
+ ${already ? '✓' : ''}
+
`;
+ }).join('');
+ el.querySelectorAll('.gex-user-result-item:not(.selected)').forEach(item => {
+ item.addEventListener('click', () => _gexSelectUser({ Id: item.dataset.id, DisplayName: item.dataset.name, UserPrincipalName: item.dataset.upn }));
+ });
+}
+
+function _gexSelectUser(u) {
+ if (GexState.selectedUsers.has(u.Id)) return;
+ GexState.selectedUsers.set(u.Id, u);
+ _gexRenderUserResults();
+ _gexRenderSelectedUsers();
+ _gexUpdateAddBtn();
+}
+
+function _gexDeselectUser(id) {
+ GexState.selectedUsers.delete(id);
+ _gexRenderUserResults();
+ _gexRenderSelectedUsers();
+ _gexUpdateAddBtn();
+}
+
+function _gexRenderSelectedUsers() {
+ const el = document.getElementById('gexSelectedUsers');
+ const countEl = document.getElementById('gexSelectedCount');
+ if (!el) return;
+ const users = [...GexState.selectedUsers.values()];
+ countEl.textContent = users.length;
+ if (users.length === 0) {
+ el.innerHTML = '
Noch keine Benutzer ausgewählt.
';
+ return;
+ }
+ el.innerHTML = users.map(u => `
+
+ ${escapeHtml(u.DisplayName || '—')}
+ ${escapeHtml(u.UserPrincipalName || '')}
+
+
+ `).join('');
+ el.querySelectorAll('.gex-selected-chip-remove').forEach(btn => {
+ btn.addEventListener('click', () => _gexDeselectUser(btn.dataset.id));
+ });
+}
+
+function _gexUpdateAddBtn() {
+ const btn = document.getElementById('gexAddBtn');
+ if (!btn) return;
+ btn.disabled = !(GexState.selectedGroup && GexState.selectedUsers.size > 0);
+}
+
+document.getElementById('gexAddBtn')?.addEventListener('click', async () => {
+ if (!GexState.selectedGroup) { toast('Bitte zuerst eine Gruppe auswählen.', 'warn'); return; }
+ if (GexState.selectedUsers.size === 0) { toast('Bitte mindestens einen Benutzer auswählen.', 'warn'); return; }
+
+ const btn = document.getElementById('gexAddBtn');
+ btn.disabled = true;
+ const origHtml = btn.innerHTML;
+ btn.innerHTML = '
Wird hinzugefügt…';
+
+ const userIds = [...GexState.selectedUsers.keys()];
+ try {
+ const data = await api(`/api/groups/${encodeURIComponent(GexState.selectedGroup.Id)}/members`, {
+ method: 'POST',
+ body: { userIds },
+ });
+ if (data.errors > 0) {
+ toast(`${data.success} von ${data.total} hinzugefügt, ${data.errors} Fehler.`, 'warn');
+ } else {
+ toast(`${data.success} Benutzer zu „${GexState.selectedGroup.DisplayName}" hinzugefügt.`, 'ok');
+ GexState.selectedUsers.clear();
+ _gexRenderSelectedUsers();
+ document.getElementById('gexUserSearch').value = '';
+ document.getElementById('gexUserResults').innerHTML = '
Mindestens 2 Zeichen eingeben.
';
+ }
+ } catch (e) {
+ toast(`Fehler: ${e.message}`, 'err');
+ } finally {
+ btn.innerHTML = origHtml;
+ _gexUpdateAddBtn();
+ }
+});
+
document.getElementById('gexExportBtn')?.addEventListener('click', () => {
if (!GexState.members?.users) return;
const m = GexState.members;
diff --git a/Intune/Intune-App-Manager-Web/www/help.md b/Intune/Intune-App-Manager-Web/www/help.md
index d5b20ae..2768f01 100644
--- a/Intune/Intune-App-Manager-Web/www/help.md
+++ b/Intune/Intune-App-Manager-Web/www/help.md
@@ -1,4 +1,4 @@
-# Intune App-Manager — Hilfe
+# Intune Manager — Hilfe
Web-Frontend für Microsoft Intune. Lädt Apps und Gruppen aus deinem Tenant über Microsoft Graph und macht das Verwalten von Zuweisungen schneller als im Intune-Portal selbst.
diff --git a/Intune/Intune-App-Manager-Web/www/index.html b/Intune/Intune-App-Manager-Web/www/index.html
index df997b0..d58c847 100644
--- a/Intune/Intune-App-Manager-Web/www/index.html
+++ b/Intune/Intune-App-Manager-Web/www/index.html
@@ -3,7 +3,7 @@
-
Intune App-Manager
+
Intune Manager