Build & Release MSI / build-msi (push) Canceled after 0s
- docs/App-Registration.md: alle benoetigten Graph-Berechtigungen (Kern, Geraete-Tab, Read-Only) + Auth-Konfiguration - docs/Setup-AppRegistration.ps1: legt die App-Registrierung automatisch an (Rechte, Public-Client-Flow, Redirect-URIs, optional Admin-Consent); Berechtigungs-IDs werden live aufgeloest - README/CHANGELOG verlinkt, ToolVersion + build-local auf 0.1.27 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
238 lines
10 KiB
PowerShell
238 lines
10 KiB
PowerShell
# Intune Manager - Web Edition
|
|
# Startet einen lokalen HTTP-Server und oeffnet das moderne Web-Frontend.
|
|
#
|
|
# Aufruf:
|
|
# .\Start.ps1
|
|
# .\Start.ps1 -Port 8088 -NoBrowser
|
|
# .\Start.ps1 -TenantId "..." -ClientId "..."
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[int]$Port = 8077,
|
|
# Optional: ueberschreibt die persistierten Settings nur fuer diesen Lauf.
|
|
# Wenn nicht gesetzt -> Werte aus %APPDATA%\IntuneAppManager-Web\settings.json
|
|
# (bzw. den Defaults beim Erststart) werden verwendet.
|
|
[string]$TenantId = "",
|
|
[string]$ClientId = "",
|
|
[switch]$NoBrowser,
|
|
[switch]$AutoInstall, # ohne Rueckfrage installieren
|
|
[switch]$SkipModuleCheck # Pre-Flight ueberspringen (z.B. CI)
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
|
|
# ============================================================
|
|
# Pre-Flight: PowerShell-Module pruefen / installieren
|
|
# (nur User-Scope, nichts systemweites)
|
|
# ============================================================
|
|
|
|
function Test-RequiredModules {
|
|
[CmdletBinding()]
|
|
param(
|
|
[hashtable[]]$Required,
|
|
[switch]$AutoInstall
|
|
)
|
|
|
|
Write-Host "Pruefe PowerShell-Module..." -ForegroundColor Cyan
|
|
$missing = @()
|
|
$outdated = @()
|
|
|
|
foreach ($req in $Required) {
|
|
$name = $req.Name
|
|
$minV = [version]$req.MinVersion
|
|
$available = @(Get-Module -Name $name -ListAvailable -ErrorAction SilentlyContinue)
|
|
if ($available.Count -eq 0) {
|
|
Write-Host " [FEHLT] $name (>= $minV)" -ForegroundColor Yellow
|
|
$missing += $req
|
|
} else {
|
|
$maxV = ($available | Sort-Object Version -Descending | Select-Object -First 1).Version
|
|
if ($maxV -lt $minV) {
|
|
Write-Host " [ALT] $name v$maxV (<$minV)" -ForegroundColor Yellow
|
|
$outdated += @{ Name = $name; Have = $maxV; Need = $minV }
|
|
} else {
|
|
Write-Host " [OK] $name v$maxV" -ForegroundColor Green
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($missing.Count -eq 0 -and $outdated.Count -eq 0) {
|
|
return $true
|
|
}
|
|
|
|
# NuGet-Provider sicherstellen, sonst schlaegt Install-Module schweigend fehl
|
|
$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
|
|
try {
|
|
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force -ErrorAction Stop | Out-Null
|
|
} catch {
|
|
throw "NuGet-Provider konnte nicht installiert werden: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# PSGallery als trusted markieren, sonst kommt fuer jede Install-Aktion ein Prompt
|
|
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
|
|
if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
|
|
try { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop } catch {}
|
|
}
|
|
|
|
if (-not $AutoInstall) {
|
|
Write-Host ""
|
|
Write-Host "Folgendes wird im User-Scope (CurrentUser) installiert/aktualisiert:" -ForegroundColor Cyan
|
|
foreach ($m in $missing) { Write-Host " + $($m.Name) min. $($m.MinVersion)" -ForegroundColor White }
|
|
foreach ($o in $outdated) { Write-Host " ~ $($o.Name) v$($o.Have) -> min. $($o.Need)" -ForegroundColor White }
|
|
Write-Host ""
|
|
$answer = Read-Host "Jetzt automatisch installieren? [J]a / [N]ein"
|
|
if ($answer -notmatch '^(j|J|y|Y)') {
|
|
Write-Host ""
|
|
Write-Host "Abbruch. Du kannst die Module manuell installieren:" -ForegroundColor Yellow
|
|
foreach ($m in $missing) { Write-Host " Install-Module $($m.Name) -Scope CurrentUser -Force" -ForegroundColor Gray }
|
|
foreach ($o in $outdated) { Write-Host " Update-Module $($o.Name) -Scope CurrentUser -Force" -ForegroundColor Gray }
|
|
return $false
|
|
}
|
|
}
|
|
|
|
foreach ($m in $missing) {
|
|
Write-Host ""
|
|
Write-Host "Installiere $($m.Name) (CurrentUser)..." -ForegroundColor Cyan
|
|
try {
|
|
Install-Module -Name $m.Name -MinimumVersion $m.MinVersion -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
|
|
Write-Host " -> OK" -ForegroundColor Green
|
|
} catch {
|
|
Write-Host " -> Fehler: $($_.Exception.Message)" -ForegroundColor Red
|
|
return $false
|
|
}
|
|
}
|
|
foreach ($o in $outdated) {
|
|
Write-Host ""
|
|
Write-Host "Aktualisiere $($o.Name) auf min. $($o.Need)..." -ForegroundColor Cyan
|
|
try {
|
|
Install-Module -Name $o.Name -MinimumVersion $o.Need -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
|
|
Write-Host " -> OK" -ForegroundColor Green
|
|
} catch {
|
|
Write-Host " -> Fehler: $($_.Exception.Message)" -ForegroundColor Red
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
if (-not $SkipModuleCheck) {
|
|
$required = @(
|
|
@{ Name = "Microsoft.Graph.Authentication"; MinVersion = "2.0.0" }
|
|
)
|
|
if (-not (Test-RequiredModules -Required $required -AutoInstall:$AutoInstall)) {
|
|
Write-Host ""
|
|
Write-Host "Server-Start abgebrochen � benoetigte Module fehlen." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
# Quellmodule laden (Reihenfolge wichtig)
|
|
. (Join-Path $root "src/Models.ps1")
|
|
. (Join-Path $root "src/Graph.ps1")
|
|
. (Join-Path $root "src/Api.ps1")
|
|
. (Join-Path $root "src/PolicyIO.ps1")
|
|
. (Join-Path $root "src/Router.ps1")
|
|
. (Join-Path $root "src/Server.ps1")
|
|
|
|
# Settings aus Persistenz (Datei) laden, optional per Parameter ueberschreiben
|
|
$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 �
|
|
# Connect-Endpoint und Co. lesen weiterhin $script:Config, das nach jedem
|
|
# Settings-Save aktualisiert wird.
|
|
# Im Multi-Tenant-Modus kommen TenantId/ClientId aus dem aktiven Profil.
|
|
$script:StartupConn = Get-ActiveConnection -Settings $script:Settings
|
|
$script:Config = @{
|
|
TenantId = $script:StartupConn.tenantId
|
|
ClientId = $script:StartupConn.clientId
|
|
ClientIdRo = $script:StartupConn.clientIdRo
|
|
Scopes = @($script:Settings.connection.scopes)
|
|
ScopesRo = @($script:Settings.connection.scopesRo)
|
|
WebRoot = (Join-Path $root "www")
|
|
ReportDir = (Join-Path $env:TEMP "IntuneAppManager-Reports")
|
|
}
|
|
|
|
if (-not (Test-Path $script:Config.ReportDir)) {
|
|
New-Item -ItemType Directory -Path $script:Config.ReportDir -Force | Out-Null
|
|
}
|
|
|
|
# Import/Export von Compliance Policies & Configuration Profiles benoetigt den
|
|
# Scope DeviceManagementConfiguration.* — bei Bedarf ergaenzen, damit das
|
|
# Verbindungs-Token die Policies lesen/schreiben darf.
|
|
if ($script:Config.Scopes -notcontains 'DeviceManagementConfiguration.ReadWrite.All') {
|
|
$script:Config.Scopes = @($script:Config.Scopes) + 'DeviceManagementConfiguration.ReadWrite.All'
|
|
}
|
|
if ($script:Config.ScopesRo -notcontains 'DeviceManagementConfiguration.Read.All') {
|
|
$script:Config.ScopesRo = @($script:Config.ScopesRo) + 'DeviceManagementConfiguration.Read.All'
|
|
}
|
|
|
|
# Session-State (im PowerShell-Prozess gehalten)
|
|
$script:State = [pscustomobject]@{
|
|
Connected = $false
|
|
ReadOnly = $false
|
|
Account = $null
|
|
TenantId = $null
|
|
Groups = @() # abt-hm Gruppen
|
|
RpaGroups = @()
|
|
Apps = @()
|
|
Users = @{} # cache by id
|
|
GroupMembers = @{} # cache groupId -> member ids
|
|
Session = @() # geplante Zuweisungen
|
|
}
|
|
|
|
$script:ToolVersion = "0.1.27"
|
|
$script:BuildStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
|
|
Write-Host ""
|
|
Write-Host "==============================================" -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
|
|
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
|
|
# das alte Fenster bedient weiter mit altem Code. Darum alle anderen Start.ps1-
|
|
# Prozesse DIESES Pfads vorher beenden, damit ein Neustart wirklich neu laedt.
|
|
# ============================================================
|
|
try {
|
|
$thisScript = $PSCommandPath
|
|
if ([string]::IsNullOrWhiteSpace($thisScript)) { $thisScript = Join-Path $PSScriptRoot 'Start.ps1' }
|
|
# Nur echte Server-Instanzen treffen: per -File gestartet UND auf diesen
|
|
# Start.ps1-Pfad zeigend. So werden -Command-Prozesse (Diagnose etc.), die
|
|
# den Pfad nur als String enthalten, NICHT versehentlich beendet.
|
|
$stale = @(Get-CimInstance Win32_Process -Filter "Name='powershell.exe' OR Name='pwsh.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.ProcessId -ne $PID -and $_.CommandLine -and ($_.CommandLine -like "*$thisScript*") -and ($_.CommandLine -like "*-File*") })
|
|
foreach ($p in $stale) {
|
|
Write-Host " Beende alte Instanz (PID $($p.ProcessId))..." -ForegroundColor Yellow
|
|
try { Stop-Process -Id $p.ProcessId -Force -ErrorAction Stop }
|
|
catch { Write-Host " konnte PID $($p.ProcessId) nicht beenden: $($_.Exception.Message)" -ForegroundColor DarkYellow }
|
|
}
|
|
if ($stale.Count -gt 0) { Start-Sleep -Milliseconds 800 } # kurz warten bis der Port frei ist
|
|
} catch {
|
|
Write-Host " (Konnte alte Instanzen nicht pruefen: $($_.Exception.Message))" -ForegroundColor DarkGray
|
|
}
|
|
|
|
$url = "http://localhost:$Port/"
|
|
Write-Host " URL: $url" -ForegroundColor Green
|
|
Write-Host " WebRoot: $($script:Config.WebRoot)" -ForegroundColor DarkGray
|
|
Write-Host " Reports: $($script:Config.ReportDir)" -ForegroundColor DarkGray
|
|
Write-Host " Beenden: Ctrl+C" -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
|
|
if (-not $NoBrowser) {
|
|
Start-Process $url
|
|
}
|
|
|
|
Start-WebServer -Port $Port
|