WLAN-Profile, portables OpenVPN und automatischer AD-Join
- WLAN-Tab: SSIDs (WPA2/WPA3/offen, versteckt) in Blaupausen speichern, fuer alle Benutzer anlegen, Profile eines Referenzgeraets uebernehmen - OpenVPN portabel: offizielles MSI per msiexec /a entpackt + Microsoft- signierter TAP-Treiber in tools\openvpn; Treiber wird nur temporaer geladen und danach entfernt (--disable-dco, --dev-node) - .ovpn/Zertifikate je Kunde hochladen, Benutzer/Kennwort optional - AD-Join mit DC-Test (SRV+LDAP), OU, Umbenennung, lokalen Admins, optional ueber VPN; VPN optional als Dienst fuer die erste Anmeldung - Fehlende Kennwoerter werden beim Start abgefragt und nicht gespeichert Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -11,3 +11,6 @@ packages/winget/*
|
|||||||
blueprints/*.json
|
blueprints/*.json
|
||||||
!blueprints/Beispiel__*.json
|
!blueprints/Beispiel__*.json
|
||||||
rdp/*.rdp
|
rdp/*.rdp
|
||||||
|
tools/openvpn/
|
||||||
|
vpn/*
|
||||||
|
!vpn/.gitkeep
|
||||||
|
|||||||
@@ -216,6 +216,26 @@ function Invoke-Api {
|
|||||||
$active = (powercfg /getactivescheme) -join ''
|
$active = (powercfg /getactivescheme) -join ''
|
||||||
return Send-Json $Ctx @{ active = $active; hibernate = [bool](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue).HibernateEnabled }
|
return Send-Json $Ctx @{ active = $active; hibernate = [bool](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue).HibernateEnabled }
|
||||||
}
|
}
|
||||||
|
# WLAN
|
||||||
|
'^GET /api/wifi$' { return Send-Json $Ctx (Get-WifiState) }
|
||||||
|
'^POST /api/wifi/apply$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "WLAN-Profile anlegen ($(@($b.wifi).Count))" 'Add-WifiProfiles' $b) } }
|
||||||
|
'^POST /api/wifi/import$' { return Send-Json $Ctx @(Export-WifiProfiles) }
|
||||||
|
'^POST /api/wifi/connect$' { $b = Read-Body $Ctx; $o = netsh wlan connect name="$($b.ssid)" 2>&1; return Send-Json $Ctx @{ ok = ($LASTEXITCODE -eq 0); message = ($o -join ' ') } }
|
||||||
|
'^DELETE /api/wifi$' { netsh wlan delete profile name="$($q['ssid'])" | Out-Null; return Send-Json $Ctx @{ ok = $true } }
|
||||||
|
|
||||||
|
# OpenVPN (portabel) + Domaene
|
||||||
|
'^GET /api/vpn$' {
|
||||||
|
$files = @(Get-ChildItem (Join-Path $global:KdsRoot 'vpn') -File -ErrorAction SilentlyContinue | Where-Object Name -notlike '.*' | ForEach-Object Name)
|
||||||
|
return Send-Json $Ctx ([pscustomobject]@{ status = (Get-VpnStatus); portable = (Get-OpenVpnPortableInfo); files = $files })
|
||||||
|
}
|
||||||
|
'^POST /api/vpn/build$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Portables OpenVPN erstellen' 'New-OpenVpnPortable' @{}) } }
|
||||||
|
'^POST /api/vpn/connect$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "VPN verbinden ($($b.vpn.config))" 'Connect-KdsVpn' $b) } }
|
||||||
|
'^POST /api/vpn/disconnect$' { $n = Disconnect-KdsVpn; Remove-PortableTap; return Send-Json $Ctx @{ ok = $true; stopped = $n } }
|
||||||
|
'^POST /api/vpn/service$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'VPN als Dienst einrichten' 'Install-KdsVpnService' $b) } }
|
||||||
|
'^DELETE /api/vpn/service$' { Remove-KdsVpnService; return Send-Json $Ctx @{ ok = $true } }
|
||||||
|
'^POST /api/domain/test$' { $b = Read-Body $Ctx; return Send-Json $Ctx (Test-KdsDomain $b.name) }
|
||||||
|
'^POST /api/domain/join$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Domaenenbeitritt $($b.domain.name)" 'Join-KdsDomain' $b) } }
|
||||||
|
|
||||||
'^GET /api/bloatware$' { return Send-Json $Ctx @(Get-BloatwareInventory) }
|
'^GET /api/bloatware$' { return Send-Json $Ctx @(Get-BloatwareInventory) }
|
||||||
'^POST /api/bloatware$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Bloatware entfernen' 'Remove-Bloatware' $b) } }
|
'^POST /api/bloatware$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Bloatware entfernen' 'Remove-Bloatware' $b) } }
|
||||||
'^GET /api/bitlocker$' { return Send-Json $Ctx (Get-BitLockerInfo) }
|
'^GET /api/bitlocker$' { return Send-Json $Ctx (Get-BitLockerInfo) }
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ Die Web-API ist immer mit einem zufälligen Token geschützt, das bei jedem Star
|
|||||||
- *Offline*: vom Stick installieren
|
- *Offline*: vom Stick installieren
|
||||||
- `setup.exe` wird bei Bedarf automatisch geladen; generierte XMLs landen in `office\configs\`.
|
- `setup.exe` wird bei Bedarf automatisch geladen; generierte XMLs landen in `office\configs\`.
|
||||||
- **Desktop-Icons** für alle Benutzer (öffentlicher Desktop): Web-Links (`.url`), Programmverknüpfungen (`.lnk`) und **RDP-Verbindungen** (`.rdp`: im Formular erzeugt inkl. Gateway, RemoteApp, Umleitungen, oder vorhandene .rdp-Dateien hochladen), eigene Icons hochladbar nach `icons\` (werden nach `C:\ProgramData\KDS-Agent\Icons` kopiert). Vorlagen in `config/shortcuts.json`.
|
- **Desktop-Icons** für alle Benutzer (öffentlicher Desktop): Web-Links (`.url`), Programmverknüpfungen (`.lnk`) und **RDP-Verbindungen** (`.rdp`: im Formular erzeugt inkl. Gateway, RemoteApp, Umleitungen, oder vorhandene .rdp-Dateien hochladen), eigene Icons hochladbar nach `icons\` (werden nach `C:\ProgramData\KDS-Agent\Icons` kopiert). Vorlagen in `config/shortcuts.json`.
|
||||||
|
- **WLAN**: SSIDs mit Kennwort (WPA2/WPA3/offen, versteckt, automatisch verbinden) in der Blaupause speichern und für alle Benutzer anlegen. WLAN-Profile eines Referenzgeräts lassen sich inkl. Kennwort übernehmen.
|
||||||
|
- **VPN & Domäne**:
|
||||||
|
- **OpenVPN portabel vom Stick**: „Portables OpenVPN erstellen“ lädt einmalig die offiziellen Pakete nach `tools\openvpn\` (OpenVPN aus dem MSI entpackt, ohne Installation, plus Microsoft-signierter TAP-Treiber). Beim Verbinden wird der TAP-Treiber nur temporär geladen und danach wieder entfernt.
|
||||||
|
- `.ovpn` und Zertifikate je Kunde hochladen (`vpn\`), Benutzer/Kennwort optional speichern. Leere Kennwörter werden beim Start abgefragt und nicht gespeichert.
|
||||||
|
- **AD-Join**: Erreichbarkeitstest (DC per DNS-SRV + LDAP), Beitritt mit OU, gleichzeitiger Umbenennung und lokalen Admin-Gruppen, optional über das VPN.
|
||||||
|
- Optional bleibt das VPN als **OpenVPN-Dienst** (offline vom Stick installiert) bestehen, damit die erste Domänenanmeldung außerhalb des Firmennetzes funktioniert.
|
||||||
- **Bloatware**: Scan des Geräts und Entfernen per Klick: Windows-Store-Apps (für alle Benutzer + Vorlage für neue Benutzer), Hersteller-Tools (HP, Dell, Lenovo, Acer, ASUS), Testversionen (McAfee, Norton …), vorinstallierte Office/OneNote-Versionen (Click-to-Run, z. B. `Microsoft 365 - fr-fr`) und OneDrive. Empfohlene Einträge sind vorausgewählt; der Katalog ist in `config/bloatware.json` erweiterbar. Business-Office-Versionen (Volumen/Enterprise) werden angezeigt, aber nie automatisch empfohlen.
|
- **Bloatware**: Scan des Geräts und Entfernen per Klick: Windows-Store-Apps (für alle Benutzer + Vorlage für neue Benutzer), Hersteller-Tools (HP, Dell, Lenovo, Acer, ASUS), Testversionen (McAfee, Norton …), vorinstallierte Office/OneNote-Versionen (Click-to-Run, z. B. `Microsoft 365 - fr-fr`) und OneDrive. Empfohlene Einträge sind vorausgewählt; der Katalog ist in `config/bloatware.json` erweiterbar. Business-Office-Versionen (Volumen/Enterprise) werden angezeigt, aber nie automatisch empfohlen.
|
||||||
- **System**: Computername, Zeitzone, Dateiendungen anzeigen (auch für neue Benutzer), Consumer-Features/Werbe-Apps aus.
|
- **System**: Computername, Zeitzone, Dateiendungen anzeigen (auch für neue Benutzer), Consumer-Features/Werbe-Apps aus.
|
||||||
- **Sicherheit**: Defender (Echtzeit/Cloud/PUA), Firewall, SmartScreen, UAC, SMBv1 aus, Autorun aus, LSA-Schutz, Telemetrie, RDP an/aus, lokales Admin-Konto.
|
- **Sicherheit**: Defender (Echtzeit/Cloud/PUA), Firewall, SmartScreen, UAC, SMBv1 aus, Autorun aus, LSA-Schutz, Telemetrie, RDP an/aus, lokales Admin-Konto.
|
||||||
@@ -44,6 +50,8 @@ config\shortcuts.json Verknüpfungs-Vorlagen (anpassbar)
|
|||||||
installers\ hochgeladene Setups + installers.json (Silent-Parameter)
|
installers\ hochgeladene Setups + installers.json (Silent-Parameter)
|
||||||
icons\ eigene Icons (.ico)
|
icons\ eigene Icons (.ico)
|
||||||
rdp\ hochgeladene .rdp-Dateien
|
rdp\ hochgeladene .rdp-Dateien
|
||||||
|
vpn\ hochgeladene .ovpn-Konfigurationen + Zertifikate
|
||||||
|
tools\openvpn\ portables OpenVPN (bin\, driver\, setup\) – per Klick erstellt
|
||||||
config\bloatware.json Bloatware-Katalog (anpassbar)
|
config\bloatware.json Bloatware-Katalog (anpassbar)
|
||||||
packages\winget\ optional: winget-Offlinepakete (*.msixbundle + Abhängigkeiten *.appx)
|
packages\winget\ optional: winget-Offlinepakete (*.msixbundle + Abhängigkeiten *.appx)
|
||||||
office\ setup.exe, configs\, source\ (Offline-Builds)
|
office\ setup.exe, configs\, source\ (Offline-Builds)
|
||||||
@@ -55,7 +63,7 @@ Ist der Stick schreibgeschützt, schreibt der Agent Logs und Schlüssel nach `C:
|
|||||||
|
|
||||||
## Hinweise
|
## Hinweise
|
||||||
|
|
||||||
- **Sicherheit**: Blaupausen können Klartext-Kennwörter (lokaler Admin, BitLocker-PIN) enthalten, und `bitlocker-keys\` enthält Wiederherstellungsschlüssel. Den Stick entsprechend schützen. Blaupausen (außer den Beispielen), RDP-Dateien und Schlüssel sind per `.gitignore` von der Versionsverwaltung ausgeschlossen.
|
- **Sicherheit**: Blaupausen können Klartext-Kennwörter (lokaler Admin, BitLocker-PIN, WLAN, VPN, Domäne) enthalten, und `bitlocker-keys\` enthält Wiederherstellungsschlüssel. Den Stick entsprechend schützen. VPN- und Domänen-Kennwörter können leer bleiben und werden dann beim Start abgefragt. Blaupausen (außer den Beispielen), RDP-/VPN-Dateien und Schlüssel sind per `.gitignore` von der Versionsverwaltung ausgeschlossen.
|
||||||
- BitLocker benötigt Windows 11 Pro/Enterprise/Education. Unter Home steht nur die Geräteverschlüsselung zur Verfügung.
|
- BitLocker benötigt Windows 11 Pro/Enterprise/Education. Unter Home steht nur die Geräteverschlüsselung zur Verfügung.
|
||||||
- Die Store-Installation (`msstore`) braucht Internet und kann bei Konten ohne Store-Zugang fehlschlagen.
|
- Die Store-Installation (`msstore`) braucht Internet und kann bei Konten ohne Store-Zugang fehlschlagen.
|
||||||
- Einige Einstellungen (Computername, LSA-Schutz, SMBv1, BitLocker mit Hardwaretest) werden erst nach einem Neustart wirksam.
|
- Einige Einstellungen (Computername, LSA-Schutz, SMBv1, BitLocker mit Hardwaretest) werden erst nach einem Neustart wirksam.
|
||||||
|
|||||||
@@ -468,6 +468,7 @@ $script:UploadTypes = @{
|
|||||||
installers = '.exe', '.msi', '.msix', '.msixbundle', '.appx', '.appxbundle', '.ps1', '.cmd', '.bat'
|
installers = '.exe', '.msi', '.msix', '.msixbundle', '.appx', '.appxbundle', '.ps1', '.cmd', '.bat'
|
||||||
icons = '.ico', '.exe', '.dll'
|
icons = '.ico', '.exe', '.dll'
|
||||||
rdp = '.rdp'
|
rdp = '.rdp'
|
||||||
|
vpn = '.ovpn', '.conf', '.crt', '.pem', '.key', '.p12', '.pfx'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Resolve-UploadTarget {
|
function Resolve-UploadTarget {
|
||||||
@@ -1038,6 +1039,402 @@ function Remove-Bloatware {
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region WLAN --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function New-WifiProfileXml {
|
||||||
|
param($W)
|
||||||
|
$e = [Security.SecurityElement]::Escape([string]$W.ssid)
|
||||||
|
$hex = -join ([Text.Encoding]::UTF8.GetBytes([string]$W.ssid) | ForEach-Object { $_.ToString('X2') })
|
||||||
|
$auth = switch ($W.auth) { 'WPA3SAE' { 'WPA3SAE' } 'open' { 'open' } default { 'WPA2PSK' } }
|
||||||
|
$sec = if ($auth -eq 'open') {
|
||||||
|
'<authEncryption><authentication>open</authentication><encryption>none</encryption><useOneX>false</useOneX></authEncryption>'
|
||||||
|
} else {
|
||||||
|
if ("$($W.key)".Length -lt 8) { throw "WLAN '$($W.ssid)': Kennwort muss mindestens 8 Zeichen haben" }
|
||||||
|
"<authEncryption><authentication>$auth</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption>" +
|
||||||
|
"<sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>$([Security.SecurityElement]::Escape([string]$W.key))</keyMaterial></sharedKey>"
|
||||||
|
}
|
||||||
|
@"
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
|
||||||
|
<name>$e</name>
|
||||||
|
<SSIDConfig><SSID><hex>$hex</hex><name>$e</name></SSID><nonBroadcast>$(if (Test-True $W.hidden) { 'true' } else { 'false' })</nonBroadcast></SSIDConfig>
|
||||||
|
<connectionType>ESS</connectionType>
|
||||||
|
<connectionMode>$(if ($W.auto -eq $false) { 'manual' } else { 'auto' })</connectionMode>
|
||||||
|
<MSM><security>$sec</security></MSM>
|
||||||
|
</WLANProfile>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-WifiProfiles {
|
||||||
|
param($Data)
|
||||||
|
$list = @($Data.wifi | Where-Object { $_.ssid })
|
||||||
|
if (-not $list.Count) { Write-Log 'Keine WLAN-Profile konfiguriert.' 'WARN'; return }
|
||||||
|
$tmp = Join-Path $env:TEMP "kds-wlan-$([guid]::NewGuid()).xml"
|
||||||
|
foreach ($w in $list) {
|
||||||
|
try {
|
||||||
|
[IO.File]::WriteAllText($tmp, (New-WifiProfileXml $w), (New-Object Text.UTF8Encoding($false)))
|
||||||
|
$out = netsh wlan add profile filename="$tmp" user=all 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw ($out -join ' ') }
|
||||||
|
Write-Log "WLAN '$($w.ssid)' gespeichert (alle Benutzer)" 'OK'
|
||||||
|
if (Test-True $w.connect) {
|
||||||
|
netsh wlan connect name="$($w.ssid)" | Out-Null
|
||||||
|
Write-Log " Verbindung zu '$($w.ssid)' angefordert"
|
||||||
|
}
|
||||||
|
} catch { Write-Log "WLAN '$($w.ssid)': $_" 'ERROR' }
|
||||||
|
finally { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WifiState {
|
||||||
|
$profiles = @(netsh wlan show profiles 2>$null | Where-Object { $_ -match '^\s{4}.+?\s:\s(.+)$' } | ForEach-Object { $Matches[1].Trim() })
|
||||||
|
$networks = @(netsh wlan show networks 2>$null | Where-Object { $_ -match '^SSID \d+ : (.*)$' } | ForEach-Object { $Matches[1].Trim() } | Where-Object { $_ } | Sort-Object -Unique)
|
||||||
|
$cur = netsh wlan show interfaces 2>$null | Where-Object { $_ -match '^\s+SSID\s+:\s(.+)$' } | ForEach-Object { $Matches[1].Trim() } | Select-Object -First 1
|
||||||
|
[pscustomobject]@{ profiles = $profiles; networks = $networks; connected = [string]$cur; hasWifi = [bool](Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.NdisPhysicalMedium -eq 9 }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Export-WifiProfiles {
|
||||||
|
# Liest die WLAN-Profile dieses Geraets inkl. Kennwort (fuer die Uebernahme in eine Blaupause)
|
||||||
|
$dir = Join-Path $env:TEMP "kds-wlan-export-$([guid]::NewGuid())"
|
||||||
|
New-Item $dir -ItemType Directory | Out-Null
|
||||||
|
try {
|
||||||
|
netsh wlan export profile key=clear folder="$dir" | Out-Null
|
||||||
|
@(Get-ChildItem $dir -Filter *.xml | ForEach-Object {
|
||||||
|
[xml]$x = [IO.File]::ReadAllText($_.FullName)
|
||||||
|
$sec = $x.WLANProfile.MSM.security
|
||||||
|
$a = [string]$sec.authEncryption.authentication
|
||||||
|
if ($a -notin 'WPA2PSK', 'WPA3SAE', 'open', 'WPAPSK') { return } # Enterprise-Profile ueberspringen
|
||||||
|
[pscustomobject]@{
|
||||||
|
ssid = [string]$x.WLANProfile.SSIDConfig.SSID.name
|
||||||
|
auth = $(if ($a -eq 'WPAPSK') { 'WPA2PSK' } else { $a })
|
||||||
|
key = [string]$sec.sharedKey.keyMaterial
|
||||||
|
hidden = ([string]$x.WLANProfile.SSIDConfig.nonBroadcast -eq 'true')
|
||||||
|
auto = ([string]$x.WLANProfile.connectionMode -ne 'manual')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} finally { Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region OpenVPN -----------------------------------------------------------------
|
||||||
|
|
||||||
|
function Get-KdsVpnDir { $d = Join-Path $env:ProgramData 'KDS-Agent\vpn'; New-Item $d -ItemType Directory -Force | Out-Null; $d }
|
||||||
|
|
||||||
|
function Get-PortableOvpnDir { Join-Path $global:KdsRoot 'tools\openvpn' }
|
||||||
|
|
||||||
|
function Get-OpenVpnExe {
|
||||||
|
# Portable Version vom Stick bevorzugen, sonst eine installierte
|
||||||
|
param([switch]$Installed)
|
||||||
|
$c = @("$env:ProgramFiles\OpenVPN\bin\openvpn.exe", "${env:ProgramFiles(x86)}\OpenVPN\bin\openvpn.exe")
|
||||||
|
if (-not $Installed) { $c = @((Join-Path (Get-PortableOvpnDir) 'bin\openvpn.exe')) + $c }
|
||||||
|
$c | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-OpenVpnPortableInfo {
|
||||||
|
$d = Get-PortableOvpnDir
|
||||||
|
[pscustomobject]@{
|
||||||
|
portable = Test-Path (Join-Path $d 'bin\openvpn.exe')
|
||||||
|
driver = Test-Path (Join-Path $d 'driver\OemVista.inf')
|
||||||
|
setup = [bool](Get-ChildItem (Join-Path $d 'setup') -Filter *.msi -ErrorAction SilentlyContinue)
|
||||||
|
version = [string](Get-Content (Join-Path $d 'version.txt') -ErrorAction SilentlyContinue)
|
||||||
|
installed = [bool](Get-OpenVpnExe -Installed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-OpenVpnPortable {
|
||||||
|
# Erstellt tools\openvpn auf dem Stick aus den offiziellen Paketen (einmalig, braucht Internet)
|
||||||
|
param($Data)
|
||||||
|
$dst = Get-PortableOvpnDir
|
||||||
|
$tmp = Join-Path $env:TEMP "kds-ovpn-$([guid]::NewGuid())"
|
||||||
|
New-Item "$tmp\dl", "$dst\bin", "$dst\driver", "$dst\setup" -ItemType Directory -Force | Out-Null
|
||||||
|
try {
|
||||||
|
if (-not (Install-Winget)) { throw 'winget nicht verfuegbar' }
|
||||||
|
Write-Log 'Lade offizielles OpenVPN-Paket (winget) ...'
|
||||||
|
Invoke-Winget @('download', '--id', 'OpenVPNTechnologies.OpenVPN', '--exact', '--architecture', 'x64', '--download-directory', "$tmp\dl", '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity') | Out-Null
|
||||||
|
$msi = Get-ChildItem "$tmp\dl" -Filter *.msi | Select-Object -First 1
|
||||||
|
if (-not $msi) { throw 'OpenVPN-MSI konnte nicht geladen werden' }
|
||||||
|
Write-Log "Entpacke $($msi.Name) (ohne Installation) ..."
|
||||||
|
$p = Start-Process msiexec.exe -ArgumentList "/a `"$($msi.FullName)`" /qn TARGETDIR=`"$tmp\x`"" -Wait -PassThru
|
||||||
|
if ($p.ExitCode -ne 0) { throw "msiexec /a ExitCode $($p.ExitCode)" }
|
||||||
|
Copy-Item "$tmp\x\OpenVPN\bin\*" "$dst\bin" -Force
|
||||||
|
if (Test-Path "$tmp\x\OpenVPN\ssl") { Copy-Item "$tmp\x\OpenVPN\ssl" $dst -Recurse -Force }
|
||||||
|
Get-ChildItem "$dst\setup" -Filter *.msi | Remove-Item -Force
|
||||||
|
Copy-Item $msi.FullName "$dst\setup" -Force # fuer die dauerhafte Dienst-Installation (offline)
|
||||||
|
|
||||||
|
Write-Log 'Lade TAP-Windows6-Treiber (Microsoft-signiert) ...'
|
||||||
|
$rel = Invoke-RestMethod 'https://api.github.com/repos/OpenVPN/tap-windows6/releases/latest' -UseBasicParsing
|
||||||
|
$zip = $rel.assets | Where-Object name -eq 'dist.win10.zip' | Select-Object -First 1
|
||||||
|
if (-not $zip) { throw 'dist.win10.zip im TAP-Release nicht gefunden' }
|
||||||
|
Invoke-WebRequest $zip.browser_download_url -OutFile "$tmp\tap.zip" -UseBasicParsing
|
||||||
|
Expand-Archive "$tmp\tap.zip" "$tmp\tap" -Force
|
||||||
|
$amd = Get-ChildItem "$tmp\tap" -Recurse -Directory -Filter amd64 | Select-Object -First 1
|
||||||
|
Copy-Item "$($amd.FullName)\*" "$dst\driver" -Force
|
||||||
|
$ver = if ($msi.BaseName -match '_(\d+\.\d+\.\d+)_') { $Matches[1] } else { $msi.BaseName }
|
||||||
|
Set-Content "$dst\version.txt" "OpenVPN $ver, TAP $($rel.tag_name), erstellt $(Get-Date -Format 'dd.MM.yyyy')"
|
||||||
|
Write-Log "Portables OpenVPN erstellt: $dst" 'OK'
|
||||||
|
} finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-TapAdapter {
|
||||||
|
Get-NetAdapter -IncludeHidden -ErrorAction SilentlyContinue | Where-Object InterfaceDescription -like 'TAP-Windows Adapter V9*' | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initialize-PortableTap {
|
||||||
|
# Laedt den TAP-Treiber vom Stick nur, wenn noch kein TAP-Adapter existiert
|
||||||
|
$a = Get-TapAdapter
|
||||||
|
if ($a) { return $a.Name }
|
||||||
|
$drv = Join-Path (Get-PortableOvpnDir) 'driver'
|
||||||
|
if (-not (Test-Path "$drv\devcon.exe")) { throw 'TAP-Treiber fehlt in tools\openvpn\driver - zuerst "Portables OpenVPN erstellen"' }
|
||||||
|
Write-Log 'Lade TAP-Treiber vom Stick (temporaer) ...'
|
||||||
|
$p = Start-Process "$drv\devcon.exe" -ArgumentList "install `"$drv\OemVista.inf`" tap0901" -Wait -PassThru -WindowStyle Hidden
|
||||||
|
if ($p.ExitCode -notin 0, 1) { throw "TAP-Treiberinstallation fehlgeschlagen (devcon ExitCode $($p.ExitCode))" }
|
||||||
|
Set-Content (Join-Path (Get-KdsVpnDir) 'tap-created.txt') (Get-Date)
|
||||||
|
for ($i = 0; $i -lt 10 -and -not $a; $i++) { Start-Sleep 1; $a = Get-TapAdapter }
|
||||||
|
if (-not $a) { throw 'TAP-Adapter wurde nicht angelegt' }
|
||||||
|
Rename-NetAdapter -Name $a.Name -NewName 'KDS-VPN' -ErrorAction SilentlyContinue
|
||||||
|
Write-Log 'TAP-Adapter "KDS-VPN" bereit' 'OK'
|
||||||
|
return 'KDS-VPN'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-PortableTap {
|
||||||
|
# Entfernt nur einen vom Agent angelegten TAP-Adapter samt Treiber
|
||||||
|
$flag = Join-Path (Get-KdsVpnDir) 'tap-created.txt'
|
||||||
|
if (-not (Test-Path $flag)) { return }
|
||||||
|
$devcon = Join-Path (Get-PortableOvpnDir) 'driver\devcon.exe'
|
||||||
|
if (Test-Path $devcon) { Start-Process $devcon -ArgumentList 'remove tap0901' -Wait -WindowStyle Hidden | Out-Null }
|
||||||
|
Get-WindowsDriver -Online -ErrorAction SilentlyContinue | Where-Object { $_.OriginalFileName -match 'oemvista\.inf$' -and $_.ProviderName -match 'TAP' } | ForEach-Object {
|
||||||
|
pnputil /delete-driver $_.Driver /uninstall /force | Out-Null
|
||||||
|
}
|
||||||
|
Remove-Item $flag -Force
|
||||||
|
Write-Log 'Portabler TAP-Treiber wieder entfernt' 'OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-OpenVpn {
|
||||||
|
# Dauerhafte Installation (nur fuer den Dienstbetrieb noetig) - bevorzugt offline vom Stick
|
||||||
|
$exe = Get-OpenVpnExe -Installed
|
||||||
|
if ($exe) { return $exe }
|
||||||
|
$msi = Get-ChildItem (Join-Path (Get-PortableOvpnDir) 'setup') -Filter *.msi -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if ($msi) {
|
||||||
|
Write-Log "Installiere OpenVPN vom Stick ($($msi.Name)) ..."
|
||||||
|
$p = Start-Process msiexec.exe -ArgumentList "/i `"$($msi.FullName)`" /qn /norestart" -Wait -PassThru
|
||||||
|
if ($p.ExitCode -notin 0, 3010) { throw "OpenVPN-MSI ExitCode $($p.ExitCode)" }
|
||||||
|
} else {
|
||||||
|
Write-Log 'Kein OpenVPN-Setup auf dem Stick - installiere ueber winget ...'
|
||||||
|
if (-not (Install-Winget)) { throw 'winget nicht verfuegbar' }
|
||||||
|
Invoke-Winget @('install', '--id', 'OpenVPNTechnologies.OpenVPN', '--exact', '--silent', '--scope', 'machine', '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity') | Out-Null
|
||||||
|
}
|
||||||
|
$exe = Get-OpenVpnExe -Installed
|
||||||
|
if (-not $exe) { throw 'OpenVPN-Installation fehlgeschlagen' }
|
||||||
|
Write-Log 'OpenVPN installiert' 'OK'
|
||||||
|
return $exe
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-OpenVpnProcesses {
|
||||||
|
@(Get-CimInstance Win32_Process -Filter "Name='openvpn.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match 'kds-openvpn' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VpnStatus {
|
||||||
|
$log = Join-Path (Get-KdsVpnDir) 'kds-openvpn.log'
|
||||||
|
$procs = Get-OpenVpnProcesses
|
||||||
|
$tail = if (Test-Path $log) { @(Get-Content $log -Tail 12 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_ }) } else { @() }
|
||||||
|
$connected = $procs.Count -gt 0 -and (Test-Path $log) -and (Select-String -Path $log -Pattern 'Initialization Sequence Completed' -Quiet)
|
||||||
|
# nur den vom Agent genutzten Adapter auswerten (andere VPN-Clients ignorieren)
|
||||||
|
$devFile = Join-Path (Get-KdsVpnDir) 'kds-dev.txt'
|
||||||
|
$ip = @()
|
||||||
|
if ($connected -and (Test-Path $devFile)) {
|
||||||
|
$ip = @(Get-NetIPAddress -InterfaceAlias ([string](Get-Content $devFile)) -AddressFamily IPv4 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_.IPAddress })
|
||||||
|
}
|
||||||
|
$inst = Get-OpenVpnExe -Installed
|
||||||
|
$kdsSvc = $inst -and [bool](Get-ChildItem (Join-Path (Split-Path (Split-Path $inst)) 'config-auto') -Filter 'kds-*.ovpn' -ErrorAction SilentlyContinue)
|
||||||
|
$svc = Get-Service OpenVPNService -ErrorAction SilentlyContinue
|
||||||
|
[pscustomobject]@{
|
||||||
|
installed = [bool]$inst; running = $procs.Count -gt 0; connected = [bool]$connected; ip = ($ip -join ', ')
|
||||||
|
kdsService = $kdsSvc; service = $(if ($kdsSvc -and $svc) { "$($svc.Status) / $($svc.StartType)" } else { '' }); log = ($tail -join "`n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VpnAdapters {
|
||||||
|
@(Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' -and $_.InterfaceDescription -match 'TAP-Windows|Wintun|OpenVPN|Data Channel Offload' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-OvpnPath {
|
||||||
|
param([string]$Name)
|
||||||
|
if (-not $Name) { throw 'Keine VPN-Konfiguration (.ovpn) ausgewaehlt' }
|
||||||
|
$p = Join-Path $global:KdsRoot "vpn\$(Split-Path $Name -Leaf)"
|
||||||
|
if (-not (Test-Path $p)) { throw "VPN-Konfiguration nicht gefunden: $p" }
|
||||||
|
return $p
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-VpnAuthFile {
|
||||||
|
param([string]$Path, $V)
|
||||||
|
Set-Content -Path $Path -Value @($V.user, $V.password) -Encoding ASCII
|
||||||
|
# nur SYSTEM und Administratoren duerfen die Datei lesen
|
||||||
|
icacls "$Path" /inheritance:r /grant:r '*S-1-5-18:F' '*S-1-5-32-544:F' | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Connect-KdsVpn {
|
||||||
|
param($Data)
|
||||||
|
$v = $Data.vpn
|
||||||
|
$cfg = Get-OvpnPath $v.config
|
||||||
|
if ((Get-VpnStatus).connected) { Write-Log 'VPN ist bereits verbunden.' 'OK'; return }
|
||||||
|
Disconnect-KdsVpn | Out-Null
|
||||||
|
$exe = Get-OpenVpnExe
|
||||||
|
if (-not $exe) { throw 'Kein OpenVPN gefunden - im Tab "VPN & Domaene" zuerst "Portables OpenVPN erstellen"' }
|
||||||
|
Write-Log "OpenVPN: $exe"
|
||||||
|
$dev = Initialize-PortableTap
|
||||||
|
Set-Content (Join-Path (Get-KdsVpnDir) 'kds-dev.txt') $dev
|
||||||
|
|
||||||
|
$dir = Get-KdsVpnDir
|
||||||
|
$log = Join-Path $dir 'kds-openvpn.log'
|
||||||
|
$auth = Join-Path $dir 'kds-openvpn-auth.txt'
|
||||||
|
$a = @('--config', "`"$cfg`"", '--cd', "`"$(Split-Path $cfg)`"", '--log', "`"$log`"", '--verb', '3', '--setenv', 'KDS', 'kds-openvpn',
|
||||||
|
'--disable-dco', '--dev-node', "`"$dev`"") # TAP-Adapter statt DCO (DCO-Treiber waere nicht portabel)
|
||||||
|
if ($v.user) { Write-VpnAuthFile $auth $v; $a += '--auth-user-pass', "`"$auth`"" }
|
||||||
|
Write-Log "Starte OpenVPN mit $(Split-Path $cfg -Leaf) ..."
|
||||||
|
$p = Start-Process $exe -ArgumentList $a -WindowStyle Hidden -PassThru
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddSeconds(90)
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
Start-Sleep 2
|
||||||
|
$txt = if (Test-Path $log) { Get-Content $log -Raw -ErrorAction SilentlyContinue } else { '' }
|
||||||
|
if ($txt -match 'Initialization Sequence Completed') { break }
|
||||||
|
if ($txt -match 'AUTH_FAILED') { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue; throw 'VPN-Anmeldung fehlgeschlagen (AUTH_FAILED) - Benutzer/Kennwort pruefen' }
|
||||||
|
if ($p.HasExited) { throw "OpenVPN wurde beendet. Log:`n$((Get-Content $log -Tail 15 -ErrorAction SilentlyContinue) -join "`n")" }
|
||||||
|
}
|
||||||
|
if (-not ((Get-Content $log -Raw -ErrorAction SilentlyContinue) -match 'Initialization Sequence Completed')) {
|
||||||
|
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
|
||||||
|
throw 'Zeitueberschreitung beim VPN-Aufbau (90 s)'
|
||||||
|
}
|
||||||
|
Start-Sleep 3
|
||||||
|
if ($v.dns) {
|
||||||
|
$servers = @($v.dns -split '[,; ]+' | Where-Object { $_ })
|
||||||
|
Get-VpnAdapters | ForEach-Object { Set-DnsClientServerAddress -InterfaceIndex $_.ifIndex -ServerAddresses $servers }
|
||||||
|
Write-Log "DNS-Server am VPN-Adapter gesetzt: $($servers -join ', ')"
|
||||||
|
}
|
||||||
|
Write-Log "VPN verbunden ($((Get-VpnStatus).ip))" 'OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Disconnect-KdsVpn {
|
||||||
|
$procs = Get-OpenVpnProcesses
|
||||||
|
$procs | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||||
|
Remove-Item (Join-Path (Get-KdsVpnDir) 'kds-openvpn-auth.txt') -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($procs.Count) { Write-Log 'VPN getrennt' 'OK' }
|
||||||
|
return $procs.Count
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-KdsVpnService {
|
||||||
|
# VPN dauerhaft ueber den OpenVPN-Dienst (config-auto) - noetig, damit die erste Domaenenanmeldung den DC erreicht
|
||||||
|
param($Data)
|
||||||
|
$v = $Data.vpn
|
||||||
|
$cfg = Get-OvpnPath $v.config
|
||||||
|
Disconnect-KdsVpn | Out-Null
|
||||||
|
Remove-PortableTap
|
||||||
|
$exe = Install-OpenVpn
|
||||||
|
$auto = Join-Path (Split-Path (Split-Path $exe)) 'config-auto'
|
||||||
|
New-Item $auto -ItemType Directory -Force | Out-Null
|
||||||
|
Get-ChildItem $auto -Filter 'kds-*' | Remove-Item -Force
|
||||||
|
|
||||||
|
$lines = @(Get-Content $cfg | Where-Object { $_ -notmatch '^\s*auth-user-pass\b' })
|
||||||
|
# referenzierte Zertifikats-/Schluesseldateien mitkopieren
|
||||||
|
foreach ($l in $lines) {
|
||||||
|
if ($l -match '^\s*(ca|cert|key|tls-auth|tls-crypt|tls-crypt-v2|pkcs12|crl-verify|dh)\s+"?([^"\s]+)"?') {
|
||||||
|
$src = Join-Path (Split-Path $cfg) $Matches[2]
|
||||||
|
if (Test-Path $src) { Copy-Item $src $auto -Force }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($v.user) {
|
||||||
|
Write-VpnAuthFile (Join-Path $auto 'kds-auth.txt') $v
|
||||||
|
$lines += 'auth-user-pass kds-auth.txt'
|
||||||
|
}
|
||||||
|
$name = 'kds-' + ([IO.Path]::GetFileNameWithoutExtension($cfg) -replace '[^\w\-]', '_') + '.ovpn'
|
||||||
|
Set-Content -Path (Join-Path $auto $name) -Value $lines -Encoding ASCII
|
||||||
|
icacls "$auto" /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null
|
||||||
|
|
||||||
|
Set-Service OpenVPNService -StartupType Automatic
|
||||||
|
Restart-Service OpenVPNService
|
||||||
|
Write-Log "VPN als Dienst eingerichtet ($name, startet automatisch vor der Anmeldung)" 'OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-KdsVpnService {
|
||||||
|
$exe = Get-OpenVpnExe -Installed
|
||||||
|
if ($exe) { Get-ChildItem (Join-Path (Split-Path (Split-Path $exe)) 'config-auto') -Filter 'kds-*' -ErrorAction SilentlyContinue | Remove-Item -Force }
|
||||||
|
Restart-Service OpenVPNService -ErrorAction SilentlyContinue
|
||||||
|
Write-Log 'VPN-Dienstkonfiguration entfernt' 'OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Domaene -----------------------------------------------------------------
|
||||||
|
|
||||||
|
function Test-KdsDomain {
|
||||||
|
param([string]$Name)
|
||||||
|
$r = [ordered]@{ domain = $Name; dns = $false; dcs = @(); ldap = $false; joined = ((Get-CimInstance Win32_ComputerSystem).Domain -eq $Name) }
|
||||||
|
try {
|
||||||
|
$srv = @(Resolve-DnsName "_ldap._tcp.dc._msdcs.$Name" -Type SRV -DnsOnly -ErrorAction Stop | Where-Object Type -eq 'SRV')
|
||||||
|
$r.dns = $srv.Count -gt 0
|
||||||
|
$r.dcs = @($srv | ForEach-Object { [string]$_.NameTarget })
|
||||||
|
if ($r.dcs) {
|
||||||
|
$c = New-Object Net.Sockets.TcpClient
|
||||||
|
$r.ldap = $c.ConnectAsync($r.dcs[0], 389).Wait(3000) -and $c.Connected
|
||||||
|
$c.Close()
|
||||||
|
}
|
||||||
|
} catch { $r.error = $_.Exception.Message }
|
||||||
|
[pscustomobject]$r
|
||||||
|
}
|
||||||
|
|
||||||
|
function Join-KdsDomain {
|
||||||
|
param($Data)
|
||||||
|
$d = $Data.domain
|
||||||
|
if (-not $d.name) { throw 'Keine Domaene angegeben' }
|
||||||
|
if ((Get-CimInstance Win32_ComputerSystem).Domain -eq $d.name) { Write-Log "Bereits Mitglied von $($d.name)" 'OK'; return }
|
||||||
|
|
||||||
|
if (Test-True $d.useVpn) { Connect-KdsVpn $Data }
|
||||||
|
|
||||||
|
Write-Log "Pruefe Erreichbarkeit von $($d.name) ..."
|
||||||
|
$t = Test-KdsDomain $d.name
|
||||||
|
if (-not $t.dns) { throw "Domaenencontroller nicht per DNS auffindbar (_ldap._tcp.dc._msdcs.$($d.name)). VPN/DNS pruefen. $($t.error)" }
|
||||||
|
Write-Log " DCs: $($t.dcs -join ', ') - LDAP erreichbar: $($t.ldap)"
|
||||||
|
|
||||||
|
$user = [string]$d.user
|
||||||
|
if ($user -notmatch '[\\@]') { $user = "$($d.name)\$user" }
|
||||||
|
$cred = New-Object PSCredential($user, (ConvertTo-SecureString ([string]$d.password) -AsPlainText -Force))
|
||||||
|
$p = @{ DomainName = $d.name; Credential = $cred; Force = $true; ErrorAction = 'Stop' }
|
||||||
|
if ($d.ou) { $p.OUPath = $d.ou }
|
||||||
|
$newName = $Data.system.computerName
|
||||||
|
if ($newName -and $newName -ne $env:COMPUTERNAME) { $p.NewName = $newName; $p.Options = 'JoinWithNewName', 'AccountCreate' }
|
||||||
|
|
||||||
|
Write-Log "Trete Domaene $($d.name) bei$(if ($d.ou) { " (OU: $($d.ou))" })$(if ($p.NewName) { " als $($p.NewName)" }) ..."
|
||||||
|
Add-Computer @p
|
||||||
|
Write-Log "Domaenenbeitritt erfolgreich - Neustart erforderlich" 'OK'
|
||||||
|
|
||||||
|
# Domaenengruppen/-benutzer zu lokalen Administratoren
|
||||||
|
$admins = @("$($d.admins)" -split '[,;\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
if ($admins.Count) {
|
||||||
|
$grp = (Get-LocalGroup -SID 'S-1-5-32-544').Name
|
||||||
|
$pending = @()
|
||||||
|
foreach ($a in $admins) {
|
||||||
|
$m = if ($a -match '\\') { $a } else { "$($d.name.Split('.')[0])\$a" }
|
||||||
|
net localgroup "$grp" "$m" /add 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) { Write-Log " $m -> lokale Administratoren" 'OK' } else { $pending += $m }
|
||||||
|
}
|
||||||
|
if ($pending) {
|
||||||
|
# nach dem Neustart per einmaliger Aufgabe nachholen
|
||||||
|
$cmd = ($pending | ForEach-Object { "net localgroup `"$grp`" `"$_`" /add" }) -join ' & '
|
||||||
|
schtasks /Create /TN 'KDS-LocalAdmins' /SC ONSTART /RU SYSTEM /RL HIGHEST /F /TR "cmd /c $cmd & schtasks /Delete /TN KDS-LocalAdmins /F" | Out-Null
|
||||||
|
Write-Log " Lokale Admins werden nach dem Neustart gesetzt: $($pending -join ', ')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-True $d.useVpn) -and (Test-True $d.vpnService)) { Install-KdsVpnService $Data }
|
||||||
|
if (Test-True $d.reboot) { $global:KdsRebootAfter = $true }
|
||||||
|
if ((Test-True $d.reboot) -and -not $global:KdsInProfile) {
|
||||||
|
shutdown /r /t 60 /c "KDS Install Agent: Neustart nach Domaenenbeitritt in 60 Sekunden"
|
||||||
|
Write-Log 'Neustart in 60 Sekunden (abbrechen: shutdown /a)' 'WARN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Gesamtprofil ------------------------------------------------------------
|
#region Gesamtprofil ------------------------------------------------------------
|
||||||
|
|
||||||
function Invoke-FullProfile {
|
function Invoke-FullProfile {
|
||||||
@@ -1049,7 +1446,9 @@ function Invoke-FullProfile {
|
|||||||
Write-Log "===== $title ====="
|
Write-Log "===== $title ====="
|
||||||
try { & $block } catch { Write-Log "$title fehlgeschlagen: $_" 'ERROR' }
|
try { & $block } catch { Write-Log "$title fehlgeschlagen: $_" 'ERROR' }
|
||||||
}
|
}
|
||||||
|
$global:KdsInProfile = $true
|
||||||
if ('system' -in $steps) { & $run 'System' { Set-SystemConfig $Data } }
|
if ('system' -in $steps) { & $run 'System' { Set-SystemConfig $Data } }
|
||||||
|
if ('wifi' -in $steps) { & $run 'WLAN' { Add-WifiProfiles $Data } }
|
||||||
if ('bloatware' -in $steps) { & $run 'Bloatware entfernen' { Remove-Bloatware $Data } }
|
if ('bloatware' -in $steps) { & $run 'Bloatware entfernen' { Remove-Bloatware $Data } }
|
||||||
if ('security' -in $steps) { & $run 'Sicherheit' { Set-SecurityConfig $Data } }
|
if ('security' -in $steps) { & $run 'Sicherheit' { Set-SecurityConfig $Data } }
|
||||||
if ('power' -in $steps) { & $run 'Energie' { Set-PowerConfig $Data } }
|
if ('power' -in $steps) { & $run 'Energie' { Set-PowerConfig $Data } }
|
||||||
@@ -1058,8 +1457,17 @@ function Invoke-FullProfile {
|
|||||||
if ('shortcuts' -in $steps) { & $run 'Desktop-Verknuepfungen' { Set-DesktopShortcuts $Data } }
|
if ('shortcuts' -in $steps) { & $run 'Desktop-Verknuepfungen' { Set-DesktopShortcuts $Data } }
|
||||||
if ('drivers' -in $steps) { & $run 'Hersteller-Treiber' { Invoke-VendorDriverUpdate $Data } }
|
if ('drivers' -in $steps) { & $run 'Hersteller-Treiber' { Invoke-VendorDriverUpdate $Data } }
|
||||||
if ('updates' -in $steps) { & $run 'Windows Update' { Install-WindowsUpdates ([pscustomobject]@{ all = $true; type = 'all' }) | Out-Null } }
|
if ('updates' -in $steps) { & $run 'Windows Update' { Install-WindowsUpdates ([pscustomobject]@{ all = $true; type = 'all' }) | Out-Null } }
|
||||||
|
if ('domain' -in $steps) { & $run 'VPN & Domaenenbeitritt' { Join-KdsDomain $Data } }
|
||||||
if ('bitlocker' -in $steps) { & $run 'BitLocker' { Set-BitLockerConfig $Data } }
|
if ('bitlocker' -in $steps) { & $run 'BitLocker' { Set-BitLockerConfig $Data } }
|
||||||
|
# Portables VPN wieder abbauen (ausser es laeuft als Dienst weiter)
|
||||||
|
if ((Get-OpenVpnProcesses).Count -or (Test-Path (Join-Path (Get-KdsVpnDir) 'tap-created.txt'))) {
|
||||||
|
& $run 'VPN trennen' { Disconnect-KdsVpn | Out-Null; Remove-PortableTap }
|
||||||
|
}
|
||||||
Write-Log 'Profil abgeschlossen.' 'OK'
|
Write-Log 'Profil abgeschlossen.' 'OK'
|
||||||
|
if ($global:KdsRebootAfter) {
|
||||||
|
shutdown /r /t 120 /c "KDS Install Agent: Einrichtung abgeschlossen - Neustart in 2 Minuten"
|
||||||
|
Write-Log 'Neustart in 2 Minuten (abbrechen: shutdown /a)' 'WARN'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
x
|
||||||
+145
-3
@@ -43,6 +43,9 @@ const DEFAULTS = () => ({
|
|||||||
security: { defender: true, firewall: true, smartScreen: true, uac: false, disableSmb1: true, disableAutorun: true, lsaProtection: false, telemetryMin: false, rdp: 'keep', localAdminName: '', localAdminPassword: '' },
|
security: { defender: true, firewall: true, smartScreen: true, uac: false, disableSmb1: true, disableAutorun: true, lsaProtection: false, telemetryMin: false, rdp: 'keep', localAdminName: '', localAdminPassword: '' },
|
||||||
power: { plan: 'balanced', monitorAc: 15, monitorDc: 5, sleepAc: 0, sleepDc: 15, hibernateAc: 0, hibernateDc: 60, lidAc: 'nothing', lidDc: 'sleep', buttonAc: 'shutdown', buttonDc: 'shutdown', hibernate: '', fastStartup: 'off' },
|
power: { plan: 'balanced', monitorAc: 15, monitorDc: 5, sleepAc: 0, sleepDc: 15, hibernateAc: 0, hibernateDc: 60, lidAc: 'nothing', lidDc: 'sleep', buttonAc: 'shutdown', buttonDc: 'shutdown', hibernate: '', fastStartup: 'off' },
|
||||||
bloatware: { recommended: true, ids: [], keep: [] },
|
bloatware: { recommended: true, ids: [], keep: [] },
|
||||||
|
wifi: [],
|
||||||
|
vpn: { config: '', user: '', password: '', dns: '' },
|
||||||
|
domain: { name: '', ou: '', user: '', password: '', admins: '', useVpn: false, vpnService: false, reboot: true },
|
||||||
bitlocker: { method: 'XtsAes256', usedSpaceOnly: true, skipHardwareTest: true, usePin: false, pin: '', backupAad: false, backupAd: false },
|
bitlocker: { method: 'XtsAes256', usedSpaceOnly: true, skipHardwareTest: true, usePin: false, pin: '', backupAad: false, backupAd: false },
|
||||||
steps: ['system', 'security', 'power', 'apps', 'office', 'shortcuts', 'updates']
|
steps: ['system', 'security', 'power', 'apps', 'office', 'shortcuts', 'updates']
|
||||||
});
|
});
|
||||||
@@ -98,7 +101,7 @@ function loadState(obj) { S = deepMerge(DEFAULTS(), obj); persist(); fillForms()
|
|||||||
function showTab(t) {
|
function showTab(t) {
|
||||||
$$('#nav a').forEach(a => a.classList.toggle('active', a.dataset.tab === t));
|
$$('#nav a').forEach(a => a.classList.toggle('active', a.dataset.tab === t));
|
||||||
$$('.tab').forEach(s => s.classList.toggle('active', s.id === 'tab-' + t));
|
$$('.tab').forEach(s => s.classList.toggle('active', s.id === 'tab-' + t));
|
||||||
({ shortcuts: loadShortcuts, bitlocker: loadBitlocker, bloatware: () => { if (!bloat) scanBloat(); }, profiles: loadProfiles, jobs: pollJobs, power: loadPower, office: loadOfficeBuilds })[t]?.();
|
({ shortcuts: loadShortcuts, bitlocker: loadBitlocker, bloatware: () => { if (!bloat) scanBloat(); }, wifi: loadWifi, vpn: loadVpn, profiles: loadProfiles, jobs: pollJobs, power: loadPower, office: loadOfficeBuilds })[t]?.();
|
||||||
}
|
}
|
||||||
$('#nav').onclick = e => { const a = e.target.closest('a'); if (a) showTab(a.dataset.tab); };
|
$('#nav').onclick = e => { const a = e.target.closest('a'); if (a) showTab(a.dataset.tab); };
|
||||||
|
|
||||||
@@ -117,6 +120,8 @@ function renderSummary() {
|
|||||||
$('#summary').innerHTML = [
|
$('#summary').innerHTML = [
|
||||||
`<b>${arr(S.apps).length}</b> Programme`, `Office: <b>${esc(prod?.name || o.product)}</b> (${o.arch}-Bit, ${esc(S.officeMode)})`,
|
`<b>${arr(S.apps).length}</b> Programme`, `Office: <b>${esc(prod?.name || o.product)}</b> (${o.arch}-Bit, ${esc(S.officeMode)})`,
|
||||||
`<b>${arr(S.shortcuts).length}</b> Desktop-Icons`, `Bloatware: <b>${S.bloatware.recommended ? 'Empfohlene' : ''}${S.bloatware.recommended && arr(S.bloatware.ids).length ? ' + ' : ''}${arr(S.bloatware.ids).length ? arr(S.bloatware.ids).length + ' gewählt' : (S.bloatware.recommended ? '' : 'keine')}</b>`, `Energieplan: <b>${esc(S.power.plan || 'unverändert')}</b>`,
|
`<b>${arr(S.shortcuts).length}</b> Desktop-Icons`, `Bloatware: <b>${S.bloatware.recommended ? 'Empfohlene' : ''}${S.bloatware.recommended && arr(S.bloatware.ids).length ? ' + ' : ''}${arr(S.bloatware.ids).length ? arr(S.bloatware.ids).length + ' gewählt' : (S.bloatware.recommended ? '' : 'keine')}</b>`, `Energieplan: <b>${esc(S.power.plan || 'unverändert')}</b>`,
|
||||||
|
arr(S.wifi).length ? `<b>${arr(S.wifi).length}</b> WLAN(s)` : '',
|
||||||
|
S.domain.name ? `Domäne: <b>${esc(S.domain.name)}</b>${S.domain.useVpn ? ' (per VPN)' : ''}` : '',
|
||||||
S.system.computerName ? `Neuer Name: <b>${esc(S.system.computerName)}</b>` : ''
|
S.system.computerName ? `Neuer Name: <b>${esc(S.system.computerName)}</b>` : ''
|
||||||
].filter(Boolean).join(' · ');
|
].filter(Boolean).join(' · ');
|
||||||
}
|
}
|
||||||
@@ -125,7 +130,9 @@ $('#btnRunAll').onclick = () => guard(async () => {
|
|||||||
if (!S.steps.length) return toast('Keine Schritte ausgewählt', true);
|
if (!S.steps.length) return toast('Keine Schritte ausgewählt', true);
|
||||||
if (S.steps.includes('bitlocker')) S.bitlocker.action = 'enable';
|
if (S.steps.includes('bitlocker')) S.bitlocker.action = 'enable';
|
||||||
if (!confirm(`Einrichtung mit ${S.steps.length} Schritten jetzt starten?`)) return;
|
if (!confirm(`Einrichtung mit ${S.steps.length} Schritten jetzt starten?`)) return;
|
||||||
const r = await api('POST', '/api/run-profile', S); jobStarted(r.job);
|
const data = await withSecrets(S, S.steps.includes('domain'), S.steps.includes('domain') && S.domain.useVpn);
|
||||||
|
if (!data) return;
|
||||||
|
const r = await api('POST', '/api/run-profile', data); jobStarted(r.job);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Programme
|
// ---------------------------------------------------------------- Programme
|
||||||
@@ -452,6 +459,141 @@ $('#btnVendor').onclick = () => guard(async () => jobStarted((await api('POST',
|
|||||||
$('#btnWinget').onclick = () => guard(async () => jobStarted((await api('POST', '/api/winget/install', {})).job));
|
$('#btnWinget').onclick = () => guard(async () => jobStarted((await api('POST', '/api/winget/install', {})).job));
|
||||||
$('#btnReboot').onclick = () => { if (confirm('Computer jetzt neu starten?')) guard(() => api('POST', '/api/reboot', {})); };
|
$('#btnReboot').onclick = () => { if (confirm('Computer jetzt neu starten?')) guard(() => api('POST', '/api/reboot', {})); };
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Kennwortabfrage
|
||||||
|
function askSecret(title, text) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const d = $('#secretDlg');
|
||||||
|
$('#secretTitle').textContent = title; $('#secretText').textContent = text || ''; $('#secretInput').value = '';
|
||||||
|
d.onclose = () => resolve(d.returnValue === 'ok' ? $('#secretInput').value : null);
|
||||||
|
d.showModal(); $('#secretInput').focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Fehlende Kennwörter (nicht in der Blaupause gespeichert) nur für diesen Lauf abfragen
|
||||||
|
async function withSecrets(state, needDomain, needVpn) {
|
||||||
|
const d = JSON.parse(JSON.stringify(state));
|
||||||
|
if (needDomain) {
|
||||||
|
if (!d.domain.name || !d.domain.user) { toast('Domäne und Benutzer angeben (Tab „VPN & Domäne“)', true); return null; }
|
||||||
|
if (!d.domain.password) {
|
||||||
|
const p = await askSecret('Domänen-Kennwort', `Kennwort für ${d.domain.user} (${d.domain.name})`); if (p == null) return null; d.domain.password = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needVpn) {
|
||||||
|
if (!d.vpn.config) { toast('Keine VPN-Konfiguration ausgewählt', true); return null; }
|
||||||
|
if (d.vpn.user && !d.vpn.password) {
|
||||||
|
const p = await askSecret('VPN-Kennwort', `Kennwort für VPN-Benutzer ${d.vpn.user}`); if (p == null) return null; d.vpn.password = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- WLAN
|
||||||
|
$('#wfAuth').onchange = () => $('#wfKeyRow').classList.toggle('hidden', $('#wfAuth').value === 'open');
|
||||||
|
$('#wfShow').onclick = () => { const i = $('#wfKey'); i.type = i.type === 'password' ? 'text' : 'password'; };
|
||||||
|
$('#btnWfAdd').onclick = () => {
|
||||||
|
const w = { ssid: $('#wfSsid').value.trim(), auth: $('#wfAuth').value, key: $('#wfKey').value, auto: $('#wfAuto').checked, hidden: $('#wfHidden').checked, connect: $('#wfConnect').checked };
|
||||||
|
if (!w.ssid) return toast('SSID angeben', true);
|
||||||
|
if (w.auth !== 'open' && w.key.length < 8) return toast('Kennwort muss mind. 8 Zeichen haben', true);
|
||||||
|
S.wifi = arr(S.wifi).filter(x => x.ssid !== w.ssid).concat(w); persist(); renderWifi();
|
||||||
|
$('#wfSsid').value = ''; $('#wfKey').value = '';
|
||||||
|
};
|
||||||
|
const AUTH_NAMES = { WPA2PSK: 'WPA2', WPA3SAE: 'WPA3', open: 'offen' };
|
||||||
|
function renderWifi() {
|
||||||
|
const l = arr(S.wifi);
|
||||||
|
$('#wfCount').textContent = l.length;
|
||||||
|
$('#wfList').innerHTML = l.length ? l.map((w, i) => `<div class="item"><span>📶</span><div class="grow"><div>${esc(w.ssid)}</div>
|
||||||
|
<div class="sub">${AUTH_NAMES[w.auth] || w.auth}${w.hidden ? ' · versteckt' : ''}${w.auto === false ? ' · manuell' : ' · automatisch'}${w.connect ? ' · verbinden' : ''}</div></div>
|
||||||
|
<button class="small ghost" data-wedit="${i}" title="Bearbeiten">✎</button><button class="small ghost" data-wdel="${i}">✕</button></div>`).join('')
|
||||||
|
: '<p class="muted">Keine WLANs in der Blaupause.</p>';
|
||||||
|
}
|
||||||
|
$('#wfList').onclick = e => {
|
||||||
|
const { wdel, wedit } = e.target.dataset;
|
||||||
|
if (wdel != null) { S.wifi.splice(wdel, 1); persist(); renderWifi(); }
|
||||||
|
if (wedit != null) {
|
||||||
|
const w = S.wifi[wedit];
|
||||||
|
$('#wfSsid').value = w.ssid; $('#wfAuth').value = w.auth; $('#wfKey').value = w.key || '';
|
||||||
|
$('#wfAuto').checked = w.auto !== false; $('#wfHidden').checked = !!w.hidden; $('#wfConnect').checked = !!w.connect;
|
||||||
|
$('#wfAuth').onchange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$('#btnWfApply').onclick = () => guard(async () => {
|
||||||
|
if (!arr(S.wifi).length) return toast('Keine WLANs in der Blaupause', true);
|
||||||
|
jobStarted((await api('POST', '/api/wifi/apply', { wifi: S.wifi })).job);
|
||||||
|
});
|
||||||
|
async function loadWifi() {
|
||||||
|
await guard(async () => {
|
||||||
|
const s = await api('GET', '/api/wifi');
|
||||||
|
$('#wfState').innerHTML = s.hasWifi ? (s.connected ? `Verbunden mit <b>${esc(s.connected)}</b>` : 'Nicht mit einem WLAN verbunden') : 'Kein WLAN-Adapter gefunden.';
|
||||||
|
$('#wfNetworks').innerHTML = arr(s.networks).map(n => `<option value="${esc(n)}">`).join('');
|
||||||
|
$('#wfDevice').innerHTML = arr(s.profiles).length ? arr(s.profiles).map(p => `<div class="item"><div class="grow">${esc(p)}</div>
|
||||||
|
<button class="small" data-wconn="${esc(p)}">Verbinden</button><button class="small danger" data-wrm="${esc(p)}">✕</button></div>`).join('')
|
||||||
|
: '<p class="muted small">Keine Profile gespeichert.</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('#wfDevice').onclick = e => guard(async () => {
|
||||||
|
const { wconn, wrm } = e.target.dataset;
|
||||||
|
if (wconn) { const r = await api('POST', '/api/wifi/connect', { ssid: wconn }); toast(r.ok ? `Verbinde mit ${wconn} …` : r.message, !r.ok); setTimeout(loadWifi, 4000); }
|
||||||
|
if (wrm && confirm(`WLAN-Profil "${wrm}" von diesem Gerät löschen?`)) { await api('DELETE', '/api/wifi?ssid=' + encodeURIComponent(wrm)); loadWifi(); }
|
||||||
|
});
|
||||||
|
$('#btnWfRefresh').onclick = loadWifi;
|
||||||
|
$('#btnWfImport').onclick = () => guard(async () => {
|
||||||
|
const list = arr(await api('POST', '/api/wifi/import', {}));
|
||||||
|
if (!list.length) return toast('Keine übernehmbaren WLAN-Profile gefunden', true);
|
||||||
|
const names = new Set(list.map(w => w.ssid));
|
||||||
|
S.wifi = arr(S.wifi).filter(x => !names.has(x.ssid)).concat(list); persist(); renderWifi();
|
||||||
|
toast(`${list.length} WLAN(s) übernommen – Blaupause speichern nicht vergessen`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- VPN & Domäne
|
||||||
|
async function loadVpn() {
|
||||||
|
await guard(async () => {
|
||||||
|
const v = await api('GET', '/api/vpn');
|
||||||
|
const p = v.portable;
|
||||||
|
$('#vpnPortable').innerHTML = p.portable && p.driver
|
||||||
|
? `<span class="okline">✓ Portables OpenVPN vorhanden</span> – ${esc(p.version)} <button class="small ghost" id="btnVpnBuild">aktualisieren</button>`
|
||||||
|
: `<span class="errline">Portables OpenVPN fehlt auf dem Stick.</span> <button class="small primary" id="btnVpnBuild">Jetzt erstellen</button>
|
||||||
|
<br>Lädt einmalig die offiziellen Pakete (OpenVPN + Microsoft-signierter TAP-Treiber) nach <code>tools\\openvpn\\</code>. Danach läuft alles ohne Installation vom Stick.`;
|
||||||
|
$('#btnVpnBuild').onclick = () => guard(async () => jobStarted((await api('POST', '/api/vpn/build', {})).job));
|
||||||
|
const files = arr(v.files).filter(f => /\.(ovpn|conf)$/i.test(f));
|
||||||
|
$('#vpnConfig').innerHTML = '<option value="">– keine –</option>' + files.map(f => `<option value="${esc(f)}">${esc(f)}</option>`).join('');
|
||||||
|
$('#vpnConfig').value = files.includes(S.vpn.config) ? S.vpn.config : '';
|
||||||
|
const s = v.status;
|
||||||
|
$('#vpnStatus').innerHTML = `<div>${s.connected ? `<span class="okline">● Verbunden</span> ${esc(s.ip)}` : s.running ? '<span style="color:var(--warn)">● Verbindungsaufbau …</span>' : '<span class="muted">○ Nicht verbunden</span>'}
|
||||||
|
${s.kdsService ? `<span class="muted small"> · Als Dienst eingerichtet: ${esc(s.service)}</span> <button class="small ghost" id="btnVpnSvcDel">Dienst-Konfiguration entfernen</button>` : ''}</div>
|
||||||
|
${s.log ? `<details><summary class="muted small">OpenVPN-Log</summary><pre class="code">${esc(s.log)}</pre></details>` : ''}`;
|
||||||
|
const del = $('#btnVpnSvcDel');
|
||||||
|
if (del) del.onclick = () => guard(async () => { if (confirm('Dauerhafte VPN-Konfiguration vom Gerät entfernen?')) { await api('DELETE', '/api/vpn/service'); loadVpn(); } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('#vpnUpload').onchange = async e => {
|
||||||
|
const up = await uploadMany('vpn', [...e.target.files]); e.target.value = '';
|
||||||
|
const ovpn = up.find(u => /\.(ovpn|conf)$/i.test(u.name));
|
||||||
|
if (ovpn) { S.vpn.config = ovpn.name; persist(); }
|
||||||
|
loadVpn();
|
||||||
|
};
|
||||||
|
$('#btnVpnConnect').onclick = () => guard(async () => {
|
||||||
|
const d = await withSecrets(S, false, true); if (!d) return;
|
||||||
|
const r = await api('POST', '/api/vpn/connect', { vpn: d.vpn }); jobStarted(r.job, true);
|
||||||
|
$('#vpnStatus').innerHTML = '<span style="color:var(--warn)">● Verbindungsaufbau …</span>';
|
||||||
|
const j = await waitJob(r.job);
|
||||||
|
toast(j.state === 'Fertig' ? 'VPN verbunden' : 'VPN-Verbindung fehlgeschlagen – siehe Log', j.state !== 'Fertig', () => { selJob = r.job; showTab('jobs'); });
|
||||||
|
loadVpn();
|
||||||
|
});
|
||||||
|
$('#btnVpnDisconnect').onclick = () => guard(async () => { await api('POST', '/api/vpn/disconnect', {}); toast('VPN getrennt'); loadVpn(); });
|
||||||
|
$('#btnVpnRefresh').onclick = loadVpn;
|
||||||
|
$('#btnDomTest').onclick = () => guard(async () => {
|
||||||
|
if (!S.domain.name) return toast('Domäne angeben', true);
|
||||||
|
$('#domTest').textContent = 'Teste …';
|
||||||
|
const t = await api('POST', '/api/domain/test', { name: S.domain.name });
|
||||||
|
$('#domTest').innerHTML = t.joined ? '<span class="okline">✓ Gerät ist bereits Mitglied dieser Domäne</span>'
|
||||||
|
: t.dns ? `<span class="okline">✓ DCs gefunden:</span> ${arr(t.dcs).map(esc).join(', ')} · LDAP ${t.ldap ? '<span class="okline">erreichbar</span>' : '<span class="errline">nicht erreichbar</span>'}`
|
||||||
|
: `<span class="errline">✗ Kein Domänencontroller per DNS gefunden.</span> VPN verbinden oder DNS-Server für den Tunnel setzen.`;
|
||||||
|
});
|
||||||
|
$('#btnDomJoin').onclick = () => guard(async () => {
|
||||||
|
const d = await withSecrets(S, true, S.domain.useVpn); if (!d) return;
|
||||||
|
if (!confirm(`Gerät jetzt der Domäne ${d.domain.name} hinzufügen?${d.domain.reboot ? '\nDanach wird neu gestartet.' : ''}`)) return;
|
||||||
|
jobStarted((await api('POST', '/api/domain/join', d)).job);
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Bloatware
|
// ---------------------------------------------------------------- Bloatware
|
||||||
let bloat = null;
|
let bloat = null;
|
||||||
const bloatChecked = it => arr(S.bloatware.ids).includes(it.id) || (S.bloatware.recommended && it.recommended && !arr(S.bloatware.keep).includes(it.id));
|
const bloatChecked = it => arr(S.bloatware.ids).includes(it.id) || (S.bloatware.recommended && it.recommended && !arr(S.bloatware.keep).includes(it.id));
|
||||||
@@ -620,7 +762,7 @@ async function waitJob(id) {
|
|||||||
$('#btnShutdown').onclick = () => { if (confirm('Agent beenden? Laufende Aufgaben werden abgebrochen.')) api('POST', '/api/shutdown', {}).finally(() => { document.body.innerHTML = '<main><h1>Agent beendet.</h1><p>Das Fenster kann geschlossen werden.</p></main>'; }); };
|
$('#btnShutdown').onclick = () => { if (confirm('Agent beenden? Laufende Aufgaben werden abgebrochen.')) api('POST', '/api/shutdown', {}).finally(() => { document.body.innerHTML = '<main><h1>Agent beendet.</h1><p>Das Fenster kann geschlossen werden.</p></main>'; }); };
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Start
|
// ---------------------------------------------------------------- Start
|
||||||
function renderAll() { renderOffice(); renderSelectedApps(); renderPlanned(); renderSummary(); }
|
function renderAll() { renderOffice(); renderSelectedApps(); renderPlanned(); renderWifi(); renderSummary(); }
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!TOKEN) { document.body.innerHTML = '<main><h1>Kein Zugriffstoken</h1><p>Bitte die vollständige URL (mit ?token=…) aus dem Agent-Fenster öffnen.</p></main>'; return; }
|
if (!TOKEN) { document.body.innerHTML = '<main><h1>Kein Zugriffstoken</h1><p>Bitte die vollständige URL (mit ?token=…) aus dem Agent-Fenster öffnen.</p></main>'; return; }
|
||||||
bindForms(); renderAll(); fillForms(); scTypeUi();
|
bindForms(); renderAll(); fillForms(); scTypeUi();
|
||||||
|
|||||||
+102
@@ -15,6 +15,8 @@
|
|||||||
<a data-tab="apps">Programme</a>
|
<a data-tab="apps">Programme</a>
|
||||||
<a data-tab="office">Office</a>
|
<a data-tab="office">Office</a>
|
||||||
<a data-tab="shortcuts">Desktop-Icons</a>
|
<a data-tab="shortcuts">Desktop-Icons</a>
|
||||||
|
<a data-tab="wifi">WLAN</a>
|
||||||
|
<a data-tab="vpn">VPN & Domäne</a>
|
||||||
<a data-tab="bloatware">Bloatware</a>
|
<a data-tab="bloatware">Bloatware</a>
|
||||||
<a data-tab="system">System & Sicherheit</a>
|
<a data-tab="system">System & Sicherheit</a>
|
||||||
<a data-tab="power">Energie</a>
|
<a data-tab="power">Energie</a>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
<p id="bpQuickDesc" class="muted small"></p>
|
<p id="bpQuickDesc" class="muted small"></p>
|
||||||
<div class="checks" id="steps">
|
<div class="checks" id="steps">
|
||||||
<label><input type="checkbox" value="system" checked> System</label>
|
<label><input type="checkbox" value="system" checked> System</label>
|
||||||
|
<label><input type="checkbox" value="wifi"> WLAN</label>
|
||||||
<label><input type="checkbox" value="bloatware"> Bloatware</label>
|
<label><input type="checkbox" value="bloatware"> Bloatware</label>
|
||||||
<label><input type="checkbox" value="security" checked> Sicherheit</label>
|
<label><input type="checkbox" value="security" checked> Sicherheit</label>
|
||||||
<label><input type="checkbox" value="power" checked> Energie</label>
|
<label><input type="checkbox" value="power" checked> Energie</label>
|
||||||
@@ -51,6 +54,7 @@
|
|||||||
<label><input type="checkbox" value="shortcuts" checked> Desktop-Icons</label>
|
<label><input type="checkbox" value="shortcuts" checked> Desktop-Icons</label>
|
||||||
<label><input type="checkbox" value="drivers"> Hersteller-Treiber</label>
|
<label><input type="checkbox" value="drivers"> Hersteller-Treiber</label>
|
||||||
<label><input type="checkbox" value="updates" checked> Windows Update</label>
|
<label><input type="checkbox" value="updates" checked> Windows Update</label>
|
||||||
|
<label><input type="checkbox" value="domain"> VPN & Domäne</label>
|
||||||
<label><input type="checkbox" value="bitlocker"> BitLocker</label>
|
<label><input type="checkbox" value="bitlocker"> BitLocker</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="summary" class="summary"></div>
|
<div id="summary" class="summary"></div>
|
||||||
@@ -242,6 +246,96 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- WLAN -->
|
||||||
|
<section id="tab-wifi" class="tab">
|
||||||
|
<h1>WLAN</h1>
|
||||||
|
<div class="cols">
|
||||||
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>WLAN zur Blaupause hinzufügen</h2>
|
||||||
|
<div class="row" style="margin-top:0">
|
||||||
|
<label class="f" style="flex:2">SSID<input id="wfSsid" list="wfNetworks" placeholder="Netzwerkname"></label>
|
||||||
|
<label class="f">Sicherheit<select id="wfAuth"><option value="WPA2PSK">WPA2-Personal</option><option value="WPA3SAE">WPA3-Personal</option><option value="open">Offen</option></select></label>
|
||||||
|
</div>
|
||||||
|
<datalist id="wfNetworks"></datalist>
|
||||||
|
<div class="f" id="wfKeyRow">Kennwort<div class="row" style="margin:3px 0 0"><input id="wfKey" type="password" autocomplete="new-password"><button class="small ghost" id="wfShow" type="button">👁</button></div></div>
|
||||||
|
<div class="checks">
|
||||||
|
<label><input type="checkbox" id="wfAuto" checked> Automatisch verbinden</label>
|
||||||
|
<label><input type="checkbox" id="wfHidden"> Verstecktes Netzwerk</label>
|
||||||
|
<label><input type="checkbox" id="wfConnect"> Nach dem Anlegen verbinden</label>
|
||||||
|
</div>
|
||||||
|
<button class="primary" id="btnWfAdd">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>In der Blaupause <span id="wfCount" class="badge">0</span></h2>
|
||||||
|
<div id="wfList" class="list"></div>
|
||||||
|
<button class="primary" id="btnWfApply">Jetzt auf diesem Gerät anlegen</button>
|
||||||
|
<p class="muted small">Profile werden für <b>alle Benutzer</b> gespeichert. Kennwörter liegen im Klartext in der Blaupause.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Dieses Gerät <button class="small ghost" id="btnWfRefresh">↻</button></h2>
|
||||||
|
<div id="wfState" class="muted small"></div>
|
||||||
|
<h3>Gespeicherte Profile</h3>
|
||||||
|
<div id="wfDevice" class="list"></div>
|
||||||
|
<button id="btnWfImport">Profile dieses Geräts in die Blaupause übernehmen</button>
|
||||||
|
<p class="muted small">Praktisch, um WLANs vom eigenen Techniker-Notebook oder einem Referenzgerät zu übernehmen (inkl. Kennwörter; Enterprise-WLANs werden übersprungen).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- VPN & Domäne -->
|
||||||
|
<section id="tab-vpn" class="tab">
|
||||||
|
<h1>VPN & Domäne</h1>
|
||||||
|
<div class="cols">
|
||||||
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>OpenVPN (portabel vom Stick)</h2>
|
||||||
|
<div id="vpnPortable" class="muted small"></div>
|
||||||
|
<div class="f">Konfiguration (.ovpn)<div class="row" style="margin:3px 0 0"><select id="vpnConfig" data-bind="vpn.config"></select>
|
||||||
|
<label class="button">Hochladen<input type="file" id="vpnUpload" accept=".ovpn,.conf,.crt,.pem,.key,.p12,.pfx" multiple hidden></label></div></div>
|
||||||
|
<p class="muted small">Zertifikate/Schlüssel, auf die die .ovpn verweist, einfach mit hochladen (Ordner <code>vpn\</code>).</p>
|
||||||
|
<div class="row">
|
||||||
|
<label class="f">VPN-Benutzer (falls nötig)<input data-bind="vpn.user" autocomplete="off"></label>
|
||||||
|
<label class="f">VPN-Kennwort<input data-bind="vpn.password" type="password" autocomplete="new-password" placeholder="leer = beim Start fragen"></label>
|
||||||
|
</div>
|
||||||
|
<label class="f">DNS-Server für den Tunnel (optional)<input data-bind="vpn.dns" placeholder="z.B. 10.0.0.10, 10.0.0.11 – nur falls das VPN keinen DNS mitliefert"></label>
|
||||||
|
<div class="row">
|
||||||
|
<button class="primary" id="btnVpnConnect">Verbinden</button>
|
||||||
|
<button id="btnVpnDisconnect">Trennen</button>
|
||||||
|
<button class="ghost" id="btnVpnRefresh">↻ Status</button>
|
||||||
|
</div>
|
||||||
|
<div id="vpnStatus" style="margin-top:12px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Domänenbeitritt (Active Directory)</h2>
|
||||||
|
<div class="row" style="margin-top:0">
|
||||||
|
<label class="f">Domäne (FQDN)<input data-bind="domain.name" placeholder="firma.local"></label>
|
||||||
|
<button id="btnDomTest">Erreichbarkeit testen</button>
|
||||||
|
</div>
|
||||||
|
<div id="domTest" class="muted small"></div>
|
||||||
|
<label class="f">Ziel-OU (optional)<input data-bind="domain.ou" placeholder="OU=Notebooks,OU=Computer,DC=firma,DC=local"></label>
|
||||||
|
<div class="row">
|
||||||
|
<label class="f">Benutzer mit Beitrittsrecht<input data-bind="domain.user" placeholder="FIRMA\admin oder [email protected]" autocomplete="off"></label>
|
||||||
|
<label class="f">Kennwort<input data-bind="domain.password" type="password" autocomplete="new-password" placeholder="leer = beim Start fragen"></label>
|
||||||
|
</div>
|
||||||
|
<label class="f">Lokale Administratoren hinzufügen (optional)<input data-bind="domain.admins" placeholder="z.B. IT-Admins, FIRMA\Helpdesk"></label>
|
||||||
|
<p class="muted small">Neuer Computername kommt aus „System & Sicherheit“ und wird beim Beitritt gleich mit gesetzt.</p>
|
||||||
|
<div class="checks col">
|
||||||
|
<label><input type="checkbox" data-bind="domain.useVpn"> Vorher OpenVPN verbinden</label>
|
||||||
|
<label><input type="checkbox" data-bind="domain.vpnService"> VPN danach dauerhaft als Dienst einrichten (für die erste Domänenanmeldung außerhalb des Firmennetzes – installiert OpenVPN offline vom Stick)</label>
|
||||||
|
<label><input type="checkbox" data-bind="domain.reboot"> Danach neu starten</label>
|
||||||
|
</div>
|
||||||
|
<button class="primary" id="btnDomJoin">Domäne beitreten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Bloatware -->
|
<!-- Bloatware -->
|
||||||
<section id="tab-bloatware" class="tab">
|
<section id="tab-bloatware" class="tab">
|
||||||
<h1>Bloatware entfernen</h1>
|
<h1>Bloatware entfernen</h1>
|
||||||
@@ -417,6 +511,14 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<dialog id="secretDlg">
|
||||||
|
<form method="dialog">
|
||||||
|
<h2 id="secretTitle">Kennwort</h2>
|
||||||
|
<p id="secretText" class="muted small"></p>
|
||||||
|
<input id="secretInput" type="password" autocomplete="new-password" style="width:100%">
|
||||||
|
<div class="row"><button value="cancel" class="ghost">Abbrechen</button><button value="ok" class="primary">OK</button></div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -97,3 +97,7 @@ textarea { font: inherit; color: var(--text); background: var(--bg); border: 1px
|
|||||||
details summary { cursor: pointer; margin: 6px 0; }
|
details summary { cursor: pointer; margin: 6px 0; }
|
||||||
input[hidden] { display: none !important; }
|
input[hidden] { display: none !important; }
|
||||||
div.f { margin-bottom: 10px; font-size: 12px; color: var(--muted); }
|
div.f { margin-bottom: 10px; font-size: 12px; color: var(--muted); }
|
||||||
|
dialog { border: 1px solid var(--line); border-radius: 12px; background: var(--card); color: var(--text); padding: 20px; width: min(420px, 90vw); }
|
||||||
|
dialog::backdrop { background: rgba(0,0,0,.45); }
|
||||||
|
.okline { color: var(--ok); font-weight: 600; } .errline { color: var(--err); font-weight: 600; }
|
||||||
|
.checks label input { flex: none; }
|
||||||
|
|||||||
Reference in New Issue
Block a user