Compare commits

...

16 Commits

Author SHA1 Message Date
b1e0bd9548 Security: Agent-Key in config.json per Windows DPAPI verschlüsselt (v2.9.0)
agent_key liegt jetzt nicht mehr im Klartext auf der Platte, sondern via
ProtectedData.Protect (DataProtectionScope.LocalMachine) verschlüsselt —
nur das SYSTEM-Konto auf genau diesem einen Rechner kann den Wert wieder
entschlüsseln. Reines Auslesen von config.json bringt einem lokalen
Angreifer/Malware also nichts mehr.

Migration automatisch beim ersten Start von v2.9.0: erkennt das alte
Klartext-Format, verschlüsselt beim nächsten Save() automatisch — kein
manueller Eingriff nötig, läuft über den bestehenden Staged-Rollout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 14:27:56 +02:00
07fa8d6020 Security: Versions-Downgrade-Schutz im Agent-Update-Mechanismus
checkin() und downloadSetup() liefern jetzt nie eine ältere Version aus,
als der Agent laut DB/eigenem Report bereits installiert hat (clampToNotOlder).
Schützt gegen genau den Bug von heute (kaputte AGENT_VERSION env-Var führte
zu echtem Downgrade auf v2.0.0 bei 17 Agents) — selbst bei künftigen
Konfigurationsfehlern kann der Server keinen Downgrade mehr anstoßen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 14:16:55 +02:00
33e0b73f7f Fix: AGENT_VERSION-Fallback-Bug erneut behoben (Downgrade auf v2.0.0)
Beim heutigen Secrets-Cleanup von docker-compose.yml ging AGENT_VERSION
verloren (war nur ad-hoc auf dem Server gesetzt, nie versioniert) — der
Code-Fallback griff dadurch wieder auf hartcodierte Alt-Versionen zurück
('2.0.0'/'1.2.4'/'1.2.3'), wodurch 17 Agents beim Selbst-Update-Versuch
fälschlich auf v2.0.0 downgraded wurden. AGENT_VERSION jetzt fest im
docker-compose.yml-Template verankert, alle Fallback-Stellen im Code auf
eine gemeinsame FALLBACK_AGENT_VERSION-Konstante zusammengeführt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 14:14:14 +02:00
f3bd8d0910 Security: Pro-Geräte Agent-Keys statt geteiltem AGENT_API_KEY (v2.8.0)
Agent v2.8.0 tauscht beim Start automatisch den geteilten Bootstrap-Key
gegen einen individuellen Per-Device-Key (POST /api/monitoring/enroll,
idempotent). Checkin/Announcements-Poll/Setup-Download/WS-Agent-Verbindungen
validieren den Key jetzt gegen den jeweiligen Hostname — ein gestohlener
Key kann sich nicht mehr als anderer Agent ausgeben (manuell verifiziert).

Alte Agents mit dem geteilten Key funktionieren während der Übergangsphase
weiter (validateAgentKey() akzeptiert beides), damit der Rollout die Fleet
nicht abrupt bricht — Migration läuft über den bestehenden Staged-Rollout
(Test → Pilot → Produktion).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:50:02 +02:00
d513992de7 Security: Secrets aus docker-compose.yml entfernt, JWT_SECRET rotiert
docker-compose.yml lag mit allen Produktions-Secrets im Klartext im Repo
(JWT_SECRET, Anthropic-API-Key, Azure-Client-Secret, SMTP/IMAP-Passwort,
Proxmox-/GitHub-Token, Teams-Bot-Secret) — bereits vor diesem Repo in der
GitHub-History vorhanden, jetzt auf ENV-Variablen umgestellt. Echte Werte
liegen jetzt nur noch in einer .env auf dem Server (gitignored).

JWT_SECRET wurde rotiert (rein intern, erzwingt einmaliges Neu-Login).
AGENT_API_KEY und SCANNER_API_KEY bewusst NICHT rotiert, da das sofort die
gesamte Agent-/Scanner-Fleet brechen würde — wird zusammen mit der geplanten
Umstellung auf pro-Geräte-Keys gemacht. Externe Secrets (Anthropic, Azure,
Proxmox, GitHub, M365-Mailbox) müssen von Simon manuell in den jeweiligen
Portalen rotiert werden, da kein Tool-Zugriff von hier aus möglich ist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:36:14 +02:00
9a5ed02a8c Repo-Cleanup: nur noch backend/frontend/agent-cs + Root-Configs versioniert
Entfernt aus der Versionierung (bleibt lokal auf der Platte, nicht Teil der IT-Nexus-App):
domain-join-tool/, nexus-scanner/ (separates Go-Projekt), playwright-tests/,
alte PS1-Agent-Generation (agent/), .claude/, .github/workflows/, diverse
Demo-HTMLs/.docx/.zip-Altlasten im Root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:29:55 +02:00
81b1c326fc Security: WS-Rollenprüfung, JWT-Cookie statt localStorage, XSS/SSRF-Fixes, RDP-Consent-Secret
Some checks failed
IT Nexus Deploy / Build Frontend (push) Has been cancelled
IT Nexus Deploy / Deploy to Production (push) Has been cancelled
- WebSocket Shell/RDP: Rollenprüfung statt nur JWT-Gültigkeit (war: jeder eingeloggte User konnte fremde Agents per Shell/RDP übernehmen)
- JWT_SECRET: Server bricht ab statt mit unsicherem Default weiterzulaufen
- Auth: Token läuft jetzt über httpOnly-Cookie statt localStorage (XSS-Schutz gegen Session-Diebstahl)
- WS-Auth: Token nicht mehr als URL-Query-Param (landete in nginx-Logs), sondern als erste Message bzw. automatisch via Cookie
- Frontend: toter Rollen-Check (isSuperAdmin/isAdmin ohne Funktionsaufruf) in AgentDetailPage gefixt
- XSS: DOMPurify-Sanitizing für alle marked.parse()-Renderstellen (KI-Antworten, Kommentare, Knowledge Base)
- E-Mail: HTML-Escaping für alle ticket-gesteuerten Felder (auch über öffentliche Ticket-Route erreichbar)
- SSRF-Schutz beim Knowledge-Base-URL-Import (blockt private/Loopback-Adressen)
- TV-Dashboard: Shared-Key statt komplett offenem Endpoint
- Striktes Rate-Limit auf /login, must_change_password serverseitig erzwungen
- Agent (C#) v2.7.0: RDP-Consent/Disconnect/Capture verlangen jetzt ein Pro-Session-Secret (war: jeder lokale Prozess konnte Consent vortäuschen), DataDir-ACL für agent.log/status.json
- FIDO-PINs AES-256-GCM-verschlüsselt statt Klartext, Retention-Job für alte patch_commands/audit_log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:27:17 +02:00
3ea28def4c Add: Dauerhaftes Bildschirm-Übertragung-Overlay während RDP-Session (v2.6.0)
- RdpActiveIndicatorWindow: TeamViewer-Style Overlay unten rechts, solange
  RDP-Capture läuft. Zeigt Dauer der Sitzung + pulsierenden roten Punkt
- User kann per "Trennen"-Button selbst die Sitzung beenden (TCP-Signal
  an Service, cancelt Capture-Loop sofort — auch nach Screen-Wechsel)
- Overlay läuft als separater User-Prozess via SessionSpawner, wird vom
  Service direkt gekillt wenn Sitzung endet (rdp_stop oder Disconnect)
- Version: 2.5.0 → 2.6.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:06:50 +02:00
cb59cfe9cf Fix: Installer self-contained Publish + Selbstbereinigung vor Install (v2.5.0)
- dotnet publish lief zuvor mit --self-contained false → Installer nur 2MB
  ohne .NET-Runtime, schlägt auf Rechnern ohne .NET 8 fehl. Jetzt wieder
  self-contained Single-File wie historische Versionen (~53MB)
- setup.iss: Vor Installation werden jetzt automatisch verwaiste
  IT-Nexus-Agent.exe Prozesse gekillt und Reste eines laufenden
  Self-Update-Zyklus (update.cmd, heruntergeladener Installer) entfernt
- Behebt Installations-Instabilität durch File-Locks bei Upgrades

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:02:00 +02:00
e2521f405d Fix: Agent-Rollout downloadete immer globale AGENT_VERSION statt Gruppen-Ziel (v2.5.0)
- downloadSetup() nutzte process.env.AGENT_VERSION statt der Patch-Gruppen-
  Zielversion → Staged Rollout (Test/Pilot/Produktion) hatte nie funktioniert,
  jeder Agent bekam beim Auto-Update immer dieselbe globale Version
- Fix: downloadSetup() löst jetzt per ?hostname=X die Gruppen-Zielversion auf,
  exakt wie checkin() es bereits tat
- ApiService.DownloadSetupAsync() schickt jetzt den Hostnamen mit
- Version: 2.4.0 → 2.5.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:35:01 +02:00
7449b64398 Fix: RDP Consent-Dialog via WTSQueryUserToken+CreateProcessAsUser (v2.4.0)
- SessionSpawner.cs: Neuer Helper — WTSQueryUserToken + CreateProcessAsUser
  ersetzt fragilen schtasks-Ansatz für UI-Spawn aus SYSTEM-Service
- RtcService: SpawnConsentHelper + SpawnCaptureHelper nutzen jetzt SessionSpawner
- Findet aktive Console-Session automatisch, Fallback auf alle Sessions
- Version: 2.3.0 → 2.4.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 09:51:40 +02:00
99830e25f4 Fix: Remote Desktop — 60fps, Screen-Wechsel live, Consent-Dialog sichtbar (v2.3.0)
- CaptureModeRunner: Thread.Sleep(150) → 16ms (~60fps) + Skip-if-unchanged via Pixel-Hash
- RtcService: rdp_switch_screen Handler — Screen wechseln ohne Consent-Dialog
- RtcService: Fallback bei Consent-Fehler → rdp_denied statt silent capture
- RdpConsentWindow: WindowStyle=ToolWindow + ShowInTaskbar=True (jetzt in Taskleiste sichtbar)
- RemoteDesktopPanel: Screen-Dropdown auch während aktiver Verbindung nutzbar
- Version: 2.2.0 → 2.3.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 09:23:36 +02:00
0b2943087d Fix: CVE Fetch via Two-Step Pagination statt URL-Filter
NVD API unterstützt keine kombinierten Filter (Timeout bei cvssV3Severity+pubStartDate).
Lösung: Erst totalResults holen, dann die letzten 60 Einträge mit startIndex abrufen
und lokal nach Score >= 5 / High+Critical filtern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:31:53 +02:00
c276b23293 Add: WM Football Theme für TV Dashboard + Live CVEs aus NVD NIST API
- TVDashboardPage.jsx komplett auf WM Matchday Theme umgestellt (1:1 Design)
- Fußballplatz-Hintergrund (PitchBackground mit SVG-Linien), rollender Ball
- Scoreboard-Header in allen 8 Slides mit LIVE-Indikator
- Jersey-Zahlen (count-up, lime/gold/rot), Confetti bei Clean Sheet / Backup ≥95%
- FBTile ersetzt BTile, WM-Metaphern: Anstoss, Aufstellung, Fitnesstest, Fairplay, Zu Null, Spielaufbau, Abwehr, Gegner-Analyse
- Neuer Backend-Endpoint GET /api/tv/cves: fetcht live von NVD NIST API, 1h In-Memory-Cache
- CVE-Slide zeigt echte aktuelle Sicherheitslücken aus dem Internet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 12:02:34 +02:00
54114b5b9f Add: PDF-Viewer inline + Team-Fortschritt auf Übersicht-Tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 12:36:12 +02:00
50bbfc6b22 Fix: Offboarding-Checkliste nach Abteilung filtern, Rollen-Filterung pro Team
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 12:22:28 +02:00
172 changed files with 2153 additions and 21500 deletions

View File

@@ -1,27 +0,0 @@
{
"permissions": {
"additionalDirectories": [
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\types",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\services",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\components\\common",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\*",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner\\src\\screens",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\ITNexusScanner",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\utils",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\models",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\services",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\widgets",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib\\screens",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\lib",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner\\android\\app\\src\\main",
"c:\\gradle-home",
"c:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\it_nexus_scanner"
],
"allow": [
"WebFetch(domain:docs.arcticwolf.com)"
]
},
"enabledPlugins": {
"playwright-skill@playwright-skill": true
}
}

View File

@@ -1,22 +0,0 @@
{
"permissions": {
"allow": [
"*",
"Bash(*)",
"Bash(node -e:*)",
"Bash(test:*)",
"Bash(npm run dev:*)",
"Bash(npm cache clean:*)",
"Bash(docker-compose ps:*)",
"Bash(docker-compose logs:*)",
"Bash(docker-compose up:*)",
"Bash(curl:*)",
"WebFetch(domain:cereda-systems.de)",
"Bash(docker-compose down:*)",
"Bash(ls:*)",
"Bash(docker-compose build:*)",
"Bash(docker-compose restart:*)",
"Bash(docker exec:*)"
]
}
}

26
.env.example Normal file
View File

@@ -0,0 +1,26 @@
# Kopieren nach .env und mit echten Werten befüllen — .env ist in .gitignore, NIEMALS committen!
JWT_SECRET=
AGENT_API_KEY=
SCANNER_API_KEY=
ENCRYPTION_KEY=
TV_DASHBOARD_KEY=
# Microsoft 365 Mailbox (it-tool@cereda-systems.de)
SMTP_PASSWORD=
IMAP_PASSWORD=
# Microsoft Azure AD App-Registrierung
AZURE_CLIENT_SECRET=
# Anthropic Claude API
ANTHROPIC_API_KEY=
# Teams Bot
TEAMS_BOT_APP_SECRET=
# Proxmox API-Token
PROXMOX_TOKEN=
# GitHub PAT (Feedback-System)
GITHUB_TOKEN=

View File

@@ -1,64 +0,0 @@
# GitHub Actions — Deploy Workflow
## Übersicht
Der Workflow `deploy.yml` läuft automatisch bei jedem Push auf `main`.
- **Job 1 `build`**: Installiert Dependencies und baut das Frontend — rein zum Kompilierungs-Check.
- **Job 2 `deploy`**: Kopiert den Quellcode per SCP auf den Server und startet Docker neu. **Erfordert manuellen Approval** über das GitHub Environment `production`.
---
## 1. SSH Key als Secret hinterlegen
### SSH Key generieren (falls noch kein dedizierter Key vorhanden)
```bash
ssh-keygen -t ed25519 -C "github-actions-itnexus" -f ~/.ssh/github_actions_itnexus
```
Den Public Key auf dem Server hinterlegen:
```bash
cat ~/.ssh/github_actions_itnexus.pub | ssh root@192.168.0.194 "cat >> ~/.ssh/authorized_keys"
```
### Secrets in GitHub eintragen
Gehe zu: **Repository → Settings → Secrets and variables → Actions → New repository secret**
| Secret Name | Wert |
|---|---|
| `SSH_PRIVATE_KEY` | Inhalt von `~/.ssh/github_actions_itnexus` (Private Key, beginnt mit `-----BEGIN OPENSSH PRIVATE KEY-----`) |
| `SERVER_IP` | `192.168.0.194` |
---
## 2. Production Environment mit Required Reviewer einrichten
### Environment erstellen
1. Gehe zu: **Repository → Settings → Environments**
2. Klicke auf **New environment**
3. Name: `production` (exakt so, wie im Workflow hinterlegt)
4. Klicke auf **Configure environment**
### Required Reviewers setzen
1. Aktiviere **Required reviewers**
2. Füge `Simon Grüssing` (GitHub-Username) als Reviewer hinzu
3. Optional: **Prevent self-review** aktivieren wenn ein weiterer Reviewer vorhanden ist
4. Speichern mit **Save protection rules**
---
## 3. Approval-Prozess
1. Push auf `main` → Job `build` startet automatisch und läuft durch.
2. Nach erfolgreichem Build: Job `deploy` wartet auf Approval.
3. GitHub schickt eine **E-Mail-Benachrichtigung** an alle eingetragenen Reviewer.
4. Reviewer klickt in der E-Mail oder direkt im GitHub Actions Tab auf den Workflow-Run.
5. Unter **"This workflow run is waiting for a required review"** → **Review deployments** → Haken bei `production` setzen → **Approve and deploy**.
6. Erst dann startet der Deployment-Job.
> Tipp: Den Workflow-Status siehst du unter **Repository → Actions**.

View File

@@ -1,68 +0,0 @@
name: IT Nexus Deploy
on:
push:
branches:
- main
workflow_dispatch:
inputs:
deploy:
description: 'Manuell deployen?'
required: true
default: 'yes'
jobs:
build:
name: Build Frontend
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js 18
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm install
- name: Build frontend
working-directory: frontend
run: npm run build
env:
CI: false
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build
environment: production
if: github.event_name == 'workflow_dispatch'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
- name: Deploy frontend/src to server
run: |
scp -r frontend/src root@${{ secrets.SERVER_IP }}:/opt/it-nexus/frontend/src
- name: Deploy backend/src to server
run: |
scp -r backend/src root@${{ secrets.SERVER_IP }}:/opt/it-nexus/backend/src
- name: Rebuild and restart Docker containers
run: |
ssh root@${{ secrets.SERVER_IP }} "cd /opt/it-nexus && docker compose build frontend backend && docker compose up -d frontend backend"

17
.gitignore vendored
View File

@@ -35,6 +35,7 @@ agent/*.exe
agent/*.msi
agent-cs/bin/
agent-cs/obj/
agent-cs/publish/
# Uploads
backend/uploads/
@@ -47,7 +48,15 @@ installer/
*.intunewin
*.ova
*.exe
installer/
*.intunewin
*.ova
*.exe
# Nicht Teil der IT-Nexus-App (separate Tools/Demos/Scratch — nur lokal)
.claude/
.github/
agent/
domain-join-tool/
nexus-scanner/
playwright-tests/
*.docx
*.zip
*-Demo.html
demo-*.html

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,190 +0,0 @@
# AUDIT: IT Nexus Windows Agent v2.0.0
**Datum:** 06.05.2026
**Geprüft von:** Claude (automatisierter Audit)
**Status:** PHASE 2 abgeschlossen — wartet auf Freigabe für Phase 3/4
---
## Architektur-Übersicht
```
IT-NB-02 (Windows Service "IT Nexus Agent")
├── POST /api/monitoring/checkin (alle 1 Min, X-Agent-Key Auth)
│ → sendet: Systeminformationen, Software, Updates, BitLocker, Defender, Serial
│ ← empfängt: agent_version, commands[], announcements[]
├── POST /api/patch/commands/result (nach jedem Command, X-Agent-Key Auth)
├── POST /api/announcements/:id/ack-agent (nach Klick auf "Gelesen und bestätigt")
└── GET /api/monitoring/agent-setup (für Auto-Update, X-Agent-Key Auth)
```
**Kommunikation:** REST/HTTP, kein WebSocket, kein Push vom Server.
**Auth:** X-Agent-Key Header bei allen Agent-Endpoints.
**Checkin-Intervall:** 1 Minute (while-loop mit Task.Delay).
**Announcement-Dispatch:** Schtasks als eingeloggter User → WPF-Dialog → ACK bei Klick.
---
## Komponenten-Status
| Komponente | Status | Anmerkung |
|---|---|---|
| Windows Service (IT Nexus Agent) | ✅ OK | Läuft auf IT-NB-02, checkt jede Minute ein |
| Checkin-Endpoint | ✅ OK | Daten kommen an, werden gespeichert |
| OS-Felderkennung | ✅ OK | Windows 11 korrekt (Build-Nummer Fix) |
| CPU/RAM/Disk-Daten | ✅ OK | Nach MonitoringAgent.js Deploy (heute) |
| BitLocker/Defender/Serial | ✅ OK | Korrekt gemeldet |
| Patch Commands | ✅ OK | check_updates, install_updates, reboot funktionieren |
| Auto-Update Mechanismus | ✅ OK | Lädt vollständigen Installer, führt ihn silent aus |
| Ankündigungs-Anzeige | ✅ OK | WPF-Dialog erscheint beim Checkin |
| Ankündigungs-ACK | ✅ OK | Wird beim Klick auf "Gelesen und bestätigt" gesendet |
| Intune-Erkennungsregel | ✅ OK | _is1 Suffix, 32-Bit: Nein |
| Server AGENT_VERSION | ✅ OK | = 2.0.0 (heute gesetzt) |
| MonitoringAgent.js (Backend-Model) | ✅ OK | Dual-Mapping heute deployed |
---
## Gefundene und behobene Probleme (heute)
### BEHOBEN ✅ — MonitoringAgent.js nicht deployt (KRITISCH)
**Was:** Server hatte altes Model ohne Fallback-Mapping für C#-Feldnamen (`data.os_name || null` statt `data.os_name || data.os || null`).
**Folge:** os_name, cpu_usage, ram_total, disk_total waren NULL in der DB für alle C#-Agent-Geräte.
**Fix:** MonitoringAgent.js auf Server deployt (10:24 Uhr).
**Verifiziert:** DB zeigt jetzt `Windows 11 Pro (Build 26100.8246)`, cpu_usage_percent=6.4, ram_total_gb=31.28.
### BEHOBEN ✅ — Announcement-Task feuert wiederholt (HOCH)
**Was:** Schtasks-Task wurde mit Trigger +60 Minuten erstellt und nach `/run` nicht gelöscht.
**Folge:** Popup erschien 60 Minuten später erneut, auch nach "Gelesen und bestätigt".
**Fix:** Task wird jetzt 3 Sekunden nach Ausführung gelöscht.
### BEHOBEN ✅ — Falscher User-Domain für Schtasks (HOCH)
**Was:** `Environment.UserDomainName` gibt als SYSTEM den Maschinennamen zurück, nicht die Domain.
**Folge:** Schtasks-Task mit `IT-NB-02\gruessing` statt `WINKEL\gruessing` → Task würde fehlschlagen.
**Fix:** Vollständigen User direkt aus `Win32_ComputerSystem.UserName` (WMI) holen.
### BEHOBEN ✅ — Auto-Update ersetzte nur EXE, nicht DLL (KRITISCH)
**Was:** UpdateAgent downloadete PS1-Script statt EXE, und ersetzte nur die EXE. In .NET 8 liegt der Code in der DLL.
**Folge:** Auto-Update hätte nicht funktioniert.
**Fix:** DownloadSetupAsync lädt den vollständigen Installer, führt ihn `/VERYSILENT` aus.
### BEHOBEN ✅ — Alte Scheduled Tasks nicht entfernt bei Installation (MITTEL)
**Was:** Installer entfernte nicht die alten PS-Agent-Tasks (`schtasks /delete`).
**Folge:** Alter Agent lief weiter parallel zum neuen Service.
**Fix:** setup.iss entfernt jetzt alle bekannten Task-Namen beim Install.
---
## Alle Fixes implementiert (06.05.2026)
| Fix | Datei | Status |
|---|---|---|
| 1 Win11-Upgrade Domain-Präfix | CommandExecutor.cs + NotificationService.cs | ✅ |
| 2 HTTP-Fehler EnsureSuccessStatusCode | ApiService.cs | ✅ |
| 3 Versions-Vergleich semantisch | AgentWorker.cs | ✅ |
| 4 _shownIds in shown_announcements.json persistiert | NotificationService.cs | ✅ |
| 5 Playwright-Selektoren robuster | playwright-tests/check.js | ✅ |
| 6 config.json ACL auf SYSTEM+Admins | AgentWorker.cs SecureConfigFile() | ✅ |
---
## Verbleibende Probleme & Risiken
### ✅ BEHOBEN — Announcement _shownIds nicht persistiert
**Fix:** Wird jetzt in `C:\ProgramData\IT Nexus Agent\shown_announcements.json` gespeichert und beim Start geladen.
### ✅ BEHOBEN — Version-Vergleich als String
**Fix:** `System.Version.TryParse()` + `serverVer > localVer` — semantischer Vergleich.
### ✅ BEHOBEN — Win11-Upgrade-Task nutzt noch alten Domain-Präfix
**Fix:** Verwendet jetzt `NotificationService.GetLoggedOnUser()` (WMI-basiert).
### ✅ BEHOBEN — HttpClient nicht für Fehler geprüft
**Fix:** `EnsureSuccessStatusCode()` in CheckinAsync, AckAnnouncementAsync, ReportCommandResultAsync.
### ✅ BEHOBEN — config.json lesbar für alle lokalen User
**Fix:** `SecureConfigFile()` in AgentWorker.RunAsync() setzt ACL auf SYSTEM + Administrators only bei jedem Start.
### ✅ BEHOBEN — Monitoring-Modal-Button in Playwright nicht gefunden
**Fix:** Robuste Selektor-Kette mit 4 Fallbacks in check.js.
### ❌ OFFEN — MonitoringAgent.js Dual-Mapping nicht in Container committed
**Beschreibung:** MonitoringAgent.js wurde per `docker cp` in den Container kopiert, aber nicht in `docker compose build` eingebaut.
**Folge:** Beim nächsten `docker compose build backend` wird die alte Version wieder aus dem Image gebaut.
**Risiko:** HOCH — nach dem nächsten vollständigen Backend-Rebuild sind os_name etc. wieder NULL.
**Fix:** Lokale Datei bereits korrekt, Server-Datei bereits korrekt — beim nächsten Build wird die Datei aus `/opt/it-nexus/backend/src/models/MonitoringAgent.js` gelesen, die heute aktualisiert wurde. ✅ Kein Problem.
---
## Phase 3 — Endpoint-Tests (06.05.2026, ~12:50 Uhr)
| Endpoint | Test | Ergebnis |
|---|---|---|
| POST /api/monitoring/checkin | Gültiger Key | ✅ HTTP 200, alle Felder korrekt gespeichert |
| POST /api/monitoring/checkin | Falscher Key | ✅ HTTP 401 |
| POST /api/patch/commands/result | Gültiger Key | ✅ HTTP 200 |
| POST /api/patch/commands/result | Falscher Key | ✅ HTTP 401 (war vorher 200 — BEHOBEN) |
| POST /api/monitoring/announcements-poll | Gültiger Key | ✅ `{"announcements":[]}` (war "Access token required" — BEHOBEN) |
| POST /api/monitoring/announcements-poll | Falscher Key | ✅ HTTP 401 |
| GET /api/monitoring/agent-setup | Gültiger Key | ✅ HTTP 200, 2.4MB EXE |
| GET /api/monitoring/agent-setup | Kein Key | ✅ HTTP 401 |
| POST /api/announcements/:id/ack-agent | Gültiger Key | ✅ Bestätigt per Code-Review |
**Zusätzlicher Fund während Phase 3:** `releaseVersionToAll` fehlte in lokal deployed controller.js → Backend crashte. Sofort behoben.
---
## Live-Verifikation (Stand 06.05.2026, ~12:30 Uhr)
| Test | Ergebnis |
|---|---|
| Service auf IT-NB-02 läuft | ✅ Running |
| Checkin alle ~1 Minute | ✅ Log zeigt regelmäßige Checkins |
| DB: os_name = "Windows 11 Pro (Build 26100.8246)" | ✅ |
| DB: cpu_usage_percent = 6.4 | ✅ |
| DB: ram_total_gb = 31.28 | ✅ |
| DB: bitlocker_status = "encrypted" | ✅ |
| DB: hardware_serial = "5CD44318HR" | ✅ |
| Playwright: Login | ✅ |
| Playwright: Dashboard | ✅ |
| Playwright: Monitoring API (IT-NB-02) | ✅ |
| Playwright: BitLocker encrypted | ✅ |
| Playwright: Defender aktiv | ✅ |
| Playwright: Serial 5CD44318HR | ✅ |
| Playwright: Win11-Badge = 🪟 | ✅ |
| Playwright: v2.0.0 in Patch Management | ✅ |
| Playwright: Test-Gruppe 1/1 aktualisiert | ✅ |
| Playwright: Helpdesk | ✅ |
| Playwright: KI-Assistent | ✅ |
| Playwright: OS = Windows 11 | ⚠️ War null (behoben), nach nächstem Test OK |
| Playwright: Detail-Modal | ⚠️ Selektor passt nicht (UI-Bug in Test) |
| Ankündigung → WPF-Dialog erscheint | ✅ Getestet |
| Ankündigung → ACK bei Klick | ✅ Code korrekt |
| Intune: Erkennungsregel | ✅ _is1 Suffix, 32-Bit: Nein |
| Auto-Update: lädt Installer | ✅ DownloadSetupAsync korrekt |
**Gesamt: 22/24 ✅, 2/24 ⚠️ (minor)**
---
## Reparatur-Plan (Prioritäten)
| Prio | Was | Risiko wenn nicht gefixt | Aufwand |
|---|---|---|---|
| 1 | Win11-Upgrade-Task Domain-Fix | Win11-Upgrade scheitert für Entra-ID-User | 5 Min |
| 2 | HttpClient Status-Prüfung | Fehler werden still geschluckt | 15 Min |
| 3 | Version-Vergleich mit Version.Parse() | Fehler ab v2.0.10 | 5 Min |
| 4 | _shownIds in status.json persistieren | Doppelte Popups bei Neustart | 15 Min |
| 5 | Playwright-Selektor für Modal | Kein automatisierter Modal-Test | 10 Min |
| 6 | config.json ACL setzen | Agent-Key-Exposure auf kompromittierten Systemen | 10 Min |
---
## Fazit
**Das System ist produktionstauglich.**
Der kritische Bug (MonitoringAgent.js Feldmapping) wurde heute behoben und verifiziert. Alle Kernfunktionen laufen korrekt. Die verbleibenden 6 Punkte sind Verbesserungen, kein Showstopper.
**Empfehlung:** Prio-1 und Prio-2 vor Produktions-Rollout fixen (je ~5-15 Min). Danach freigeben.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,29 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "agent-cs", "agent-cs", "{80D118B3-826D-1EA8-FFCB-9D6851D86064}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IT-Nexus-Agent", "agent-cs\IT-Nexus-Agent.csproj", "{5B33A971-38EB-521D-F0EA-7EACC8511EB9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B33A971-38EB-521D-F0EA-7EACC8511EB9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5B33A971-38EB-521D-F0EA-7EACC8511EB9} = {80D118B3-826D-1EA8-FFCB-9D6851D86064}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ABDE2D4F-9A27-4FA1-8951-C75147FF36AC}
EndGlobalSection
EndGlobal

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,216 +0,0 @@
# IT Nexus System Audit — 07.05.2026
**Erstellt von:** Claude (Read-Only, keine Änderungen)
**Status:** Offen — noch nicht bearbeitet
---
## Executive Summary
Das System läuft stabil. Frontend und Backend sind erreichbar, die letzten 10+ Tage alle grün im Health-Check. Es gibt aber konkrete Probleme in den Bereichen Sicherheit, Konsistenz und Wartbarkeit.
---
## 🟢 Was gut ist
| Bereich | Status |
|---|---|
| System erreichbar (HTTPS) | ✅ |
| Health: letzte 10 Tage | ✅ alle grün |
| Agent-Auth (X-Agent-Key) | ✅ korrekt |
| JWT-Auth auf geschützten Routes | ✅ korrekt |
| Rate-Limiting auf Login | ✅ vorhanden |
| DB-Migrations (try/catch) | ✅ idempotent |
| asyncHandler auf Controllern | ✅ konsistent |
---
## 🔴 Kritische Probleme (sofort)
### 1. Agent-Version Default falsch
- `AGENT_VERSION` Fallback im Code noch auf `'1.2.4'` — zieht sich durch 3 Stellen im Controller
- **Fix:** In docker-compose.yml `AGENT_VERSION=2.0.0` — bereits gesetzt ✅. Aber Fallback im Code noch `'1.2.4'` und `'1.2.3'` — sollte auf `'2.0.0'` geändert werden
- **Dateien:** `backend/src/controllers/monitoringAgent.controller.js` Zeilen 25, 113, 136
### 2. `/api/monitoring/agent-setup` ohne vollständige Auth
- Endpoint prüft Key im Controller, aber Route liegt vor Middleware — unkonventionell
- Kein HTTPS-erzwungener Download
### 3. SSE-Endpoints mit Token im Query-Parameter
- `/api/tickets/:id/events?token=xxx` — Token landet in Server-Logs und Browser-History
- `/api/network-monitor/sse?token=xxx` — gleiches Problem
- **Dateien:** `backend/src/routes/ticket.routes.js`, `backend/src/routes/networkMonitor.routes.js`
---
## 🟡 Hohe Priorität (diese Woche)
### 4. Kein Rate-Limiting außer Login
- `POST /api/tickets/public` — jeder kann Tickets erstellen, keine Limits
- `POST /api/tickets/:id/comments` — Spam möglich
- **Datei:** `backend/src/server.js` Zeilen 64-74
### 5. Keine Pagination auf großen Endpoints
- `GET /api/users` → alle User auf einmal
- `GET /api/assets` → alle Assets auf einmal
- `GET /api/tickets` → alle Tickets
- Bei Wachstum wird das ein Performance-Problem
### 6. E-Mail-Fehler werden ignoriert
- `.catch(err => console.error())` ohne Retry
- Bestätigungs-Mails, Onboarding-Mails könnten lautlos verloren gehen
- **Dateien:** `backend/src/controllers/ticket.controller.js` Zeilen 131, 137, 178
### 7. Kein Graceful Shutdown der DB
- SQLite wird beim Neustart nicht sauber geschlossen → mögliche Locks
- **Datei:** `backend/src/server.js` (fehlt: `process.on('SIGTERM')` Handler)
---
## 🟠 Mittlere Priorität (diesen Monat)
### 8. Kein strukturiertes Logging
- Überall `console.log/error` — kein Log-Level, kein JSON-Format
- Schwer zu debuggen in Produktion
- **Empfehlung:** Winston oder Pino einführen
### 9. Planner-Sync läuft alle 10 Sekunden
- Sehr aggressiv — sollte 5 Minuten sein
- **Datei:** `backend/src/server.js` Zeile 274-290
### 10. CORS in Dev auf `*`
- Alle Ursprünge erlaubt — sollte auch in Dev eingeschränkt sein
- **Datei:** `backend/src/server.js` Zeilen 46-53
### 11. Onboarding-PII unverschlüsselt
- `emp_phone`, `emp_address`, `emp_private_email` in Klartext in DB
- DSGVO-relevant bei DB-Leak
- **Tabelle:** `onboarding_protocols`
### 12. Schema-Inkonsistenz Asset-Typen
- DB-Schema kennt 4 Typen, seed.js seeded 9 Typen — CHECK-Constraint wird umgangen
- **Datei:** `backend/src/db/seed.js`
### 13. Audit-Logging unvollständig
- Password-Änderungen nicht gelogged
- Token-Generierungen nicht gelogged
- **Empfehlung:** Alle Auth-Events in `audit_log` schreiben
---
## 📊 Vollständige Route-Übersicht
### Auth Routes (`/api/auth`)
| Methode | Pfad | Auth |
|---|---|---|
| POST | /login | Public (Rate-Limited) |
| GET | /me | JWT |
| POST | /change-password | JWT |
| PUT | /notifications | JWT |
| PUT | /staff-notifications | JWT |
| GET | /microsoft | Public |
| GET | /microsoft/callback | Public |
| GET | /microsoft/addin | Public |
| GET | /microsoft/addin-callback | Public |
| GET | /microsoft/addin-result/:sessionId | Public |
### Monitoring Routes (`/api/monitoring`)
| Methode | Pfad | Auth |
|---|---|---|
| POST | /checkin | X-Agent-Key |
| POST | /announcements-poll | X-Agent-Key |
| GET | /agent-script | X-Agent-Key |
| GET | /agent-setup | X-Agent-Key (Controller) |
| GET | /statistics | JWT + Admin |
| GET | / | JWT + Admin |
| GET | /:id | JWT + Admin |
| DELETE | /:id | JWT + Admin |
### Patch Routes (`/api/patch`)
| Methode | Pfad | Auth |
|---|---|---|
| POST | /commands/result | X-Agent-Key |
| GET | /overview | JWT |
| GET | /groups | JWT + Admin |
| POST | /groups | JWT + Admin |
| PUT | /groups/:id | JWT + Admin |
| DELETE | /groups/:id | JWT + Admin |
| POST | /commands/trigger | JWT + Admin |
| POST | /commands/trigger-group | JWT + Admin |
| GET | /commands | JWT + Admin |
---
## 📋 Datenbank-Tabellen (38 gesamt)
| Tabelle | Zweck |
|---|---|
| roles | Benutzerrollen |
| users | Benutzerkonten |
| fido_keys | FIDO2-Schlüssel |
| assets | IT-Assets |
| asset_assignments | Asset-Zuweisungen |
| asset_inspections | Inspektionen |
| asset_movements | Bewegungshistorie |
| asset_types | Asset-Kategorien |
| onboarding_protocols | Onboarding |
| offboarding_protocols | Offboarding |
| tickets | Support-Tickets |
| ticket_comments | Kommentare |
| ticket_links | Ticket-Verknüpfungen |
| ticket_assignees | Mehrfach-Zuweisungen |
| ticket_routing | Auto-Routing |
| monitoring_agents | Windows Agenten |
| network_devices | Netzwerk-Monitoring |
| device_checks | Prüf-Ergebnisse |
| health_history | System-Health |
| patch_groups | Patch-Gruppen |
| patch_policies | Patch-Richtlinien |
| patch_agent_groups | Agent-Gruppen |
| patch_commands | Patch-Befehle |
| licenses | Softwarelizenzen |
| warehouse_locations | Lagerplätze |
| stock_thresholds | Mindestbestände |
| purchase_orders | Bestellungen |
| knowledge_base | Wissensdatenbank |
| ai_knowledge | KI-Wissensdatenbank |
| portal_guides | Portal-Guides |
| announcements | Ankündigungen |
| announcement_acks | Bestätigungen |
| external_alerts | Externe Warnungen |
| teams_channels | Teams-Kanäle |
| audit_log | Audit-Protokoll |
| unifi_config | Unifi-Konfiguration |
| unifi_devices | Unifi-Geräte |
| it_topics | IT-Übersicht |
---
## 📋 Priorisierte To-Do-Liste
| Prio | Aufwand | Was | Datei |
|---|---|---|---|
| 1 | 5 Min | Code-Fallback `'1.2.4'``'2.0.0'` | monitoringAgent.controller.js |
| 2 | 30 Min | Rate-Limiting auf public Ticket-Erstellung | server.js |
| 3 | 30 Min | Pagination auf /api/users, /api/assets, /api/tickets | controllers |
| 4 | 1h | Graceful Shutdown für SQLite | server.js |
| 5 | 2h | E-Mail Retry-Logic | ticket.controller.js |
| 6 | 2h | SSE Token aus Query absichern | routes |
| 7 | 1 Tag | Strukturiertes Logging einführen | server.js + alle |
| 8 | 1 Tag | PII-Felder verschlüsseln (DSGVO) | onboarding_protocols |
---
## Live-System Stand (07.05.2026)
| | |
|---|---|
| Health letzten 10 Tage | ✅ alle OK |
| Frontend erreichbar | ✅ |
| API erreichbar | ✅ |
| Agent v2.0.0 | ✅ IT-NB-02, TBO-NB-02, FAM102223 |
| Geräte mit altem Agent | ⚠️ ~41 Geräte noch v1.x |
| CT 110 (AI) | ✅ nach fsck repariert |
| CT 102, 103, 111 | ✅ alle healthy |
---
*Nächster Schritt: Priorisierung mit Simon besprechen, dann schrittweise abarbeiten.*

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +0,0 @@
# ZELO 5.3 — Installationsanleitung
**Winkel Ruf-Leitsysteme | Version 5.3.0.28**
---
## Voraussetzungen
- Windows 10/11 (x64)
- SQL Server 2019 Express muss bereits installiert sein (siehe ZELO 6.0 Anleitung, Phase 1)
- Setup-Datei: `SetupZeloConfig.exe`
- Pfad: `C:\Winkel Soft\02 Zelo (5.3_5.4_6.0)\01 ZELO5.3\Winkel_Soft\01 ZELO Setup\zeloSetup 5.3.0.28\`
- Installer: `SetupZeloConfig.exe` (1.553 KB)
- Administratorrechte auf dem Zielrechner
---
## Installation
### Schritt 1 — Startmenü
- `SetupZeloConfig.exe` starten
- Zwei Optionen erscheinen:
- **"zelo Config"** → auswählen
- "Microsoft® SQL Server® 2019 Express" → **nicht auswählen** (bereits installiert)
### Schritt 2 — AGB akzeptieren
- Checkbox **"Ich habe die AGB der Winkel GmbH gelesen und akzeptiere diese"** aktivieren ✅
- Klick: **Weiter**
### Schritt 3 — Eingaben zur Installation
- Funktion: **Server** auswählen (USB/CAN-Adapter ist an diesem PC angeschlossen)
- SQL-Servername: **`localhost\ZELO`**
- Benutzer: **`sa`** (manuell eingeben)
- Passwort: **`Cered@ZeloFL24`** (manuell eingeben)
- Zielverzeichnis: `C:\zeloConfig` (Standard lassen)
- Klick: **Verbindungstest** — grünes Symbol muss erscheinen ✅
- Klick: **Weiter**
> ⚠️ Alle Felder müssen manuell befüllt werden — werden nicht automatisch übernommen.
### Schritt 4 — Komponenten auswählen
- Alle drei Komponenten aktivieren:
-**zelo Config, Benutzeroberfläche**
-**zelo Server, Windows Dienst zur Anbindung des zelo Systems über den CAN-Adapter**
-**zelo DB, Beispieldatenbank**
- Klick: **Weiter**
### Schritt 5 — Zusammenfassung prüfen
- Angaben kontrollieren:
- Art der Lizenz: `zelo Server`
- SQLServer: `localhost\ZELO`
- Benutzer: `sa`
- Zielverzeichnis: `C:\zeloConfig`
- Klick: **"zelo Config jetzt installieren"**
### Schritt 6 — Installation läuft
- Fortschrittsbalken läuft durch
- Warten bis "Installation ist abgeschlossen..." erscheint
- Klick: **Weiter**
### Schritt 7 — Abschließen
- Haken so lassen:
- ✅ Desktopverknüpfung erstellen
- ✅ Eintrag im Startmenü hinzufügen
- ☐ Bei Computerstart automatisch starten (leer lassen)
- Klick: **Fertigstellen**
---
## Zugangsdaten
| Parameter | Wert |
|---|---|
| SQL-Instanz | `localhost\ZELO` |
| SA-Benutzer | `sa` |
| SA-Passwort | `Cered@ZeloFL24` |
| Installationsverzeichnis | `C:\zeloConfig` |
---
*Erstellt: Mai 2026 | Cereda Systems GmbH*

View File

@@ -1,163 +0,0 @@
# ZELO 6.0 — Installationsanleitung
**Winkel Ruf-Leitsysteme | Version 6.0.0.62**
---
## Voraussetzungen
- Windows 10/11 (x64)
- Setup-Datei: `Setup_ZELO.exe` (V6.0.0.62)
- Administratorrechte auf dem Zielrechner
---
## Phase 0: Vorbereitung — PowerShell (vor dem Setup!)
> ⚠️ Dieser Schritt muss **vor** dem Start des Setups ausgeführt werden, da Windows Dateien von Netzlaufwerken/USB blockiert. Das Setup öffnet sich danach automatisch.
**PowerShell als Administrator** öffnen und folgenden Befehl ausführen:
```powershell
# ZELO Setup Installer
# Dateien entsperren und Setup starten
$setupPath = 'C:\Winkel Soft\02 Zelo (5.3_5.4_6.0)\03 ZELO6.0 (inkl 5.4)\Winkel_Soft\01 ZELO Setup\ZeloSetup 6.0.0.62'
Write-Host "Entsperre alle Dateien..." -ForegroundColor Cyan
Get-ChildItem -Path $setupPath -Recurse | Unblock-File
Write-Host "Starte Setup..." -ForegroundColor Green
Start-Process -FilePath "$setupPath\Setup_ZELO.exe" -Verb RunAs -Wait
Write-Host "Fertig!" -ForegroundColor Green
```
Das Script:
1. Entsperrt alle Dateien im Setup-Ordner (`Unblock-File`)
2. Startet `Setup_ZELO.exe` automatisch mit Administratorrechten
3. Wartet bis das Setup abgeschlossen ist
---
## Phase 1: SQL Server 2019 installieren
### Schritt 1 — System Check
- `Setup_ZELO.exe` als Administrator starten
- Installationstyp: **SQL Server** auswählen
- Modus: **Einzelplatzinstallation**
- Klick: **Weiter**
### Schritt 2 — Parameter
- Installationsverzeichnis: `C:\Program Files\ZELO Config` (Standard)
- SQL-Instanzname: `ZELO`
- Anmeldename: `sa`
- Passwort: `Olez_2013` *(wird später in Schritt 10 auf `Cered@ZeloFL24` geändert)*
- Klick: **Installieren**
> SQL Server 2019 Express wird jetzt im Hintergrund heruntergeladen und vorbereitet.
### Schritt 3 — SQL Server 2019 Installationsassistent
Der SQL Server 2019 Setup-Assistent öffnet sich automatisch.
### Schritt 4 — SQL Lizenzbedingungen
- Checkbox **"Ich akzeptiere die Lizenzbedingungen"** aktivieren
- Klick: **Weiter**
### Schritt 5 — Setupdateien installieren
- Warten bis der Fortschrittsbalken abgeschlossen ist und "Weiter" aktiv wird
- Klick: **Weiter**
### Schritt 6 — Installationsregeln
- Firewall-Warnung ist **normal** und kann ignoriert werden
- Klick: **Weiter**
### Schritt 7 — Funktionsauswahl
- Standard-Auswahl beibehalten: **Datenbank-Engine-Dienste** + **SQL Server-Replikation**
- Klick: **Weiter**
### Schritt 8 — Instanzkonfiguration
- Named Instance: **ZELO** (vorausgefüllt, nicht ändern)
- Instanz-ID: **ZELO** (vorausgefüllt, nicht ändern)
- Klick: **Weiter**
### Schritt 9 — Serverkonfiguration
| Dienst | Konto | Starttyp |
|---|---|---|
| SQL Server-Datenbank-Engine | `NT AUTHORITY\SYSTEM` | Automatisch |
| SQL Server-Browser | `NT AUTHORITY\LOCAL SERVICE` | Automatisch |
- Klick: **Weiter**
### Schritt 10 — Datenbank-Engine-Konfiguration
- Tab: **Serverkonfiguration**
- Authentifizierungsmodus: **Gemischter Modus** (SQL Server + Windows)
- SA-Kennwort: **`Cered@ZeloFL24`** *(beide Felder — Standardpasswort funktioniert nicht!)*
- SQL Server-Administratoren:
- **"Aktuellen Benutzer hinzufügen"** klicken
- ⚠️ Prüfen ob der **richtige User** eingetragen ist (z.B. `WINKEL\Bill`)
- Falls ein Admin-Account steht: **"Entfernen"** → **"Hinzufügen..."** → korrekten User manuell suchen
- Klick: **Weiter**
### Schritt 11 — Installationsstatus
- Warten bis die Installation abgeschlossen ist (Fortschrittsbalken)
- "Weiter" wird aktiv sobald fertig
### Schritt 12 — Abgeschlossen
- Alle Komponenten zeigen Status **"Erfolgreich"** ✅
- Details: "Installation erfolgreich"
- Klick: **Schließen**
---
## Phase 2: ZELO Software installieren
### Schritt 13 — ZELO Setup neu starten & Komponenten auswählen
- `Setup_ZELO.exe` erneut starten
- Im System Check erscheinen nun alle Komponenten
- Die oberen sind bereits installiert (.NET, Ixxat, SQL Server) — **nicht anfassen**
- Die **drei unteren** aktivieren (Haken setzen):
-**ZELO Server 6.x** (6.0.0.62)
-**ZELO Client** (6.0.0.62)
-**ZELO Server 5.4** (5.4.1.4)
- Klick: **Weiter**
### Schritt 14 — Parameter (SQL-Verbindung)
- Installationsverzeichnis: `C:\Program Files\ZELO Config` (Standard lassen)
- SQL-Instanz wird automatisch erkannt: `COMPUTERNAME\ZELO`
- Anmeldename: **`sa`**
- Passwort: **`Cered@ZeloFL24`** eintragen
- Klick: **"Verbindungstest"** — muss erfolgreich sein ✅
- Klick: **Installieren**
### Schritt 15 — Installations-Status (Abgeschlossen)
- Beide Haken können so bleiben:
- ✅ Verknüpfung auf dem Desktop erstellen
- ✅ Protokolldatei der Installation speichern (Empfohlen)
- Klick: **Fertigstellen**
---
## Phase 3: Testen
### Schritt 16 — Desktop-Verknüpfung
- Nach der Installation erscheint auf dem Desktop das Icon **"ZELO Config"**
### Schritt 17 — ZELO Config starten
- Doppelklick auf das Desktop-Icon "ZELO Config"
- Ladebildschirm erscheint — Version **6.0.0.62** wird angezeigt
- Warten bis vollständig geladen
- ✅ Installation erfolgreich wenn die Anwendung startet
---
## Zusammenfassung wichtiger Zugangsdaten
| Parameter | Wert |
|---|---|
| SQL-Instanz | `COMPUTERNAME\ZELO` |
| SA-Benutzer | `sa` |
| SA-Passwort | `Cered@ZeloFL24` |
| Installationsverzeichnis | `C:\Program Files\ZELO Config` |
---
*Erstellt: Mai 2026 | Cereda Systems GmbH*

1
_ul
View File

@@ -1 +0,0 @@
mkdir: cannot create directory C:\\gradle-home\\it_nexus_build: File exists

View File

@@ -6,7 +6,7 @@ namespace ITNexusAgent;
public class AgentWorker
{
private const string Version = "2.2.0";
private const string Version = "2.9.0";
private const string DataDir = @"C:\ProgramData\IT Nexus Agent";
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
@@ -24,6 +24,7 @@ public class AgentWorker
{
_exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
Directory.CreateDirectory(DataDir);
SecureDataDir(DataDir);
if (!File.Exists(ConfigPath))
{
@@ -39,12 +40,25 @@ public class AgentWorker
Log($"Agent v{Version} gestartet");
// Security-Migration: geteilten Bootstrap-Key gegen individuellen Per-Device-Key tauschen.
// Idempotent (Server liefert bestehenden Key erneut) — daher bei jedem Start sicher aufrufbar.
var hostname = SystemInfoService.GetHostname();
var enrolledKey = await _api.EnrollAsync(hostname);
var configWasUnencrypted = !File.ReadAllText(ConfigPath).Contains("dpapi:");
if (!string.IsNullOrEmpty(enrolledKey) && (enrolledKey != _config.AgentKey || configWasUnencrypted))
{
_config.AgentKey = enrolledKey;
_config.Save(ConfigPath); // schreibt agent_key jetzt DPAPI-verschlüsselt statt im Klartext
_api.UpdateKey(enrolledKey);
Log("ENROLL: Per-Device-Key erhalten und gespeichert");
}
// WebSocket Shell-Service im Hintergrund starten
var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname());
var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, hostname);
_ = shellService.RunAsync(_ct);
// WebRTC Remote Desktop Service im Hintergrund starten
var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname());
var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, hostname);
_ = rtcService.RunAsync(_ct);
while (!_ct.IsCancellationRequested)
@@ -176,6 +190,30 @@ public class AgentWorker
catch { }
}
// Verzeichnis-ACL: nur SYSTEM/Administratoren — verhindert dass normale lokale User
// agent.log/status.json lesen (Hostname, letzter User, RDP-/Patch-Aktivität) oder manipulieren.
private static void SecureDataDir(string dir)
{
try
{
var di = new System.IO.DirectoryInfo(dir);
var acl = di.GetAccessControl();
acl.SetAccessRuleProtection(true, false);
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
"SYSTEM", System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit,
System.Security.AccessControl.PropagationFlags.None,
System.Security.AccessControl.AccessControlType.Allow));
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
"Administrators", System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit,
System.Security.AccessControl.PropagationFlags.None,
System.Security.AccessControl.AccessControlType.Allow));
di.SetAccessControl(acl);
}
catch { }
}
private static void SecureConfigFile(string path)
{
try

View File

@@ -11,7 +11,7 @@ public static class CaptureModeRunner
private static readonly ImageCodecInfo JpegCodec =
ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
public static void Run(string portStr, int screenIdx = 0)
public static void Run(string portStr, int screenIdx, string secret)
{
if (!int.TryParse(portStr, out var port) || port <= 0) return;
@@ -20,11 +20,11 @@ public static class CaptureModeRunner
using var tcp = new TcpClient();
tcp.Connect("127.0.0.1", port);
var stream = tcp.GetStream();
stream.Write(System.Text.Encoding.ASCII.GetBytes(secret));
var encParams = new EncoderParameters(1);
encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L);
// Capture-Region bestimmen: 0 = alle Screens, 1-N = spezifischer Screen
var allScreens = System.Windows.Forms.Screen.AllScreens;
Rectangle captureRect;
if (screenIdx <= 0 || screenIdx > allScreens.Length)
@@ -40,14 +40,27 @@ public static class CaptureModeRunner
captureRect = allScreens[screenIdx - 1].Bounds;
}
uint lastHash = 0;
var sw = System.Diagnostics.Stopwatch.StartNew();
while (tcp.Connected)
{
sw.Restart();
try
{
using var bmp = new Bitmap(captureRect.Width, captureRect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(bmp))
g.CopyFromScreen(captureRect.X, captureRect.Y, 0, 0, new Size(captureRect.Width, captureRect.Height));
var hash = SampleHash(bmp);
if (hash == lastHash)
{
var elapsed = (int)sw.ElapsedMilliseconds;
if (elapsed < 16) Thread.Sleep(16 - elapsed);
continue;
}
lastHash = hash;
byte[] jpeg;
using (var ms = new MemoryStream())
{
@@ -55,16 +68,32 @@ public static class CaptureModeRunner
jpeg = ms.ToArray();
}
// 4-Byte Länge (LE) + JPEG-Daten
stream.Write(BitConverter.GetBytes(jpeg.Length));
stream.Write(jpeg);
stream.Flush();
}
catch { break; }
Thread.Sleep(150); // ~6-7 fps
var frameMs = (int)sw.ElapsedMilliseconds;
if (frameMs < 16) Thread.Sleep(16 - frameMs); // cap at ~60 fps
}
}
catch { /* Verbindung fehlgeschlagen → Exit */ }
}
private static uint SampleHash(Bitmap bmp)
{
// Sample 64 evenly-distributed pixels — fast change detection, O(1)
uint h = 2166136261u;
int w = bmp.Width, ht = bmp.Height;
int stepX = Math.Max(1, w / 8);
int stepY = Math.Max(1, ht / 8);
for (int y = 0; y < ht; y += stepY)
for (int x = 0; x < w; x += stepX)
{
var c = bmp.GetPixel(x, y);
h = (h ^ (uint)c.ToArgb()) * 16777619u;
}
return h;
}
}

View File

@@ -5,14 +5,14 @@ namespace ITNexusAgent;
// Läuft als User-Prozess (via schtasks), zeigt Consent-Dialog und sendet Antwort via TCP
public static class ConsentModeRunner
{
public static void Run(string portStr)
public static void Run(string portStr, string secret)
{
if (!int.TryParse(portStr, out var port) || port <= 0) return;
var app = new System.Windows.Application();
app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
var win = new RdpConsentWindow(port);
var win = new RdpConsentWindow(port, secret);
win.Topmost = true;
win.Show();
win.Activate();

View File

@@ -7,8 +7,8 @@
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>IT-Nexus-Agent</AssemblyName>
<RootNamespace>ITNexusAgent</RootNamespace>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<Version>2.9.0</Version>
<AssemblyVersion>2.9.0.0</AssemblyVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
@@ -31,6 +31,7 @@
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
using ITNexusAgent.UI;
namespace ITNexusAgent;
// Läuft als User-Prozess (via SessionSpawner), zeigt dauerhaftes "Bildschirm wird übertragen"-Overlay
// solange RDP-Session aktiv ist. User kann selbst trennen (TCP-Signal an Service).
public static class IndicatorModeRunner
{
public static void Run(string portStr, string secret)
{
if (!int.TryParse(portStr, out var port) || port <= 0) return;
var app = new System.Windows.Application();
app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
var win = new RdpActiveIndicatorWindow(port, secret);
win.Show();
app.Run();
}
}

View File

@@ -1,20 +1,69 @@
using Newtonsoft.Json;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace ITNexusAgent.Models;
public class AgentConfig
{
[JsonProperty("server_url")]
private const string ProtectedPrefix = "dpapi:";
[JsonIgnore]
public string ServerUrl { get; set; } = "";
[JsonProperty("agent_key")]
// Im Speicher immer Klartext — nur auf der Platte (config.json) liegt der verschlüsselte Wert.
[JsonIgnore]
public string AgentKey { get; set; } = "";
private class RawConfig
{
[JsonProperty("server_url")] public string ServerUrl { get; set; } = "";
[JsonProperty("agent_key")] public string AgentKey { get; set; } = "";
}
public static AgentConfig Load(string path)
{
var json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<AgentConfig>(json)
var raw = JsonConvert.DeserializeObject<RawConfig>(json)
?? throw new Exception("Ungültige config.json");
return new AgentConfig
{
ServerUrl = raw.ServerUrl,
AgentKey = Unprotect(raw.AgentKey),
};
}
// Verschlüsselt den Key per Windows DPAPI (LocalMachine-Scope) bevor er auf die Platte geschrieben
// wird — ein Klartext-Auslesen von config.json bringt einem Angreifer dann nichts mehr, da der Wert
// nur vom SYSTEM-Konto auf genau diesem Rechner wieder entschlüsselt werden kann.
public void Save(string path)
{
var raw = new RawConfig { ServerUrl = ServerUrl, AgentKey = Protect(AgentKey) };
File.WriteAllText(path, JsonConvert.SerializeObject(raw, Formatting.Indented));
}
private static string Protect(string plaintext)
{
if (string.IsNullOrEmpty(plaintext)) return plaintext;
var bytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(plaintext), null, DataProtectionScope.LocalMachine);
return ProtectedPrefix + Convert.ToBase64String(bytes);
}
// Erkennt das alte Klartext-Format (z.B. frisch aus dem Installer-Template) und lässt es unverändert
// durch — wird beim nächsten Save() automatisch verschlüsselt persistiert.
private static string Unprotect(string stored)
{
if (string.IsNullOrEmpty(stored) || !stored.StartsWith(ProtectedPrefix)) return stored;
try
{
var bytes = ProtectedData.Unprotect(Convert.FromBase64String(stored[ProtectedPrefix.Length..]), null, DataProtectionScope.LocalMachine);
return Encoding.UTF8.GetString(bytes);
}
catch
{
return stored; // Korrupt/falsche Maschine → unverändert zurückgeben statt Crash
}
}
}

View File

@@ -20,13 +20,22 @@ internal class Program
return;
case "--rdp-consent":
ConsentModeRunner.Run(args.Length > 1 ? args[1] : "");
ConsentModeRunner.Run(
args.Length > 1 ? args[1] : "",
args.Length > 2 ? args[2] : "");
return;
case "--rdp-capture":
CaptureModeRunner.Run(
args.Length > 1 ? args[1] : "",
args.Length > 2 && int.TryParse(args[2], out var si) ? si : 0);
args.Length > 2 && int.TryParse(args[2], out var si) ? si : 0,
args.Length > 3 ? args[3] : "");
return;
case "--rdp-indicator":
IndicatorModeRunner.Run(
args.Length > 1 ? args[1] : "",
args.Length > 2 ? args[2] : "");
return;
case "--dashboard":

View File

@@ -9,7 +9,25 @@ public class ApiService(string serverUrl, string agentKey)
{
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) };
private readonly string _baseUrl = serverUrl.TrimEnd('/');
private readonly string _agentKey = agentKey;
private string _agentKey = agentKey;
// Nach erfolgreichem Enrollment wird der geteilte Bootstrap-Key durch den individuellen
// Per-Device-Key ersetzt — alle nachfolgenden Requests dieser Instanz nutzen ab dann den neuen Key.
public void UpdateKey(string newKey) => _agentKey = newKey;
public async Task<string?> EnrollAsync(string hostname)
{
try
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/enroll", new { hostname });
var resp = await _http.SendAsync(req);
if (!resp.IsSuccessStatusCode) return null;
var body = await resp.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeAnonymousType(body, new { status = "", data = new { agent_key = "" } });
return result?.data?.agent_key;
}
catch { return null; }
}
private HttpRequestMessage BuildRequest(HttpMethod method, string path, object? body = null)
{
@@ -65,9 +83,9 @@ public class ApiService(string serverUrl, string agentKey)
return await resp.Content.ReadAsByteArrayAsync();
}
public async Task<byte[]> DownloadSetupAsync()
public async Task<byte[]> DownloadSetupAsync(string hostname)
{
var req = BuildRequest(HttpMethod.Get, "/api/monitoring/agent-setup");
var req = BuildRequest(HttpMethod.Get, $"/api/monitoring/agent-setup?hostname={Uri.EscapeDataString(hostname)}");
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsByteArrayAsync();

View File

@@ -125,7 +125,7 @@ public class CommandExecutor(ApiService api, string hostname, string dataDir, st
private async Task<string> UpdateAgent()
{
var bytes = await _api.DownloadSetupAsync();
var bytes = await _api.DownloadSetupAsync(_hostname);
if (bytes.Length < 512 * 1024) return "Download fehlgeschlagen - Installer zu klein";
var setupPath = Path.Combine(_dataDir, "IT-Nexus-Agent-Setup-Update.exe");

View File

@@ -2,6 +2,7 @@ using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -15,6 +16,9 @@ public class RtcService
private readonly string _serverUrl;
private readonly string _agentKey;
private readonly string _hostname;
private int _indicatorPid = -1;
private TcpListener? _indicatorListener;
private CancellationTokenSource? _userDisconnectCts;
public RtcService(string serverUrl, string agentKey, string hostname)
{
@@ -70,31 +74,81 @@ public class RtcService
_ = ConsentAndCaptureAsync(ws, screenIdx, captureCts.Token);
AgentWorker.Log($"RDP: Consent angefordert (screen={screenIdx})");
}
else if (type == "rdp_switch_screen" && captureCts != null)
{
// Consent bereits erteilt — nur Screen wechseln, kein erneuter Dialog
var newScreen = obj["screen"]?.ToObject<int>() ?? 0;
captureCts.Cancel();
captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
var switchToken = _userDisconnectCts != null
? CancellationTokenSource.CreateLinkedTokenSource(captureCts.Token, _userDisconnectCts.Token).Token
: captureCts.Token;
_ = CapturePipeLoopAsync(ws, newScreen, switchToken);
AgentWorker.Log($"RDP: Screen gewechselt zu {newScreen}");
}
else if (type == "rdp_stop" && captureCts != null)
{
captureCts.Cancel();
captureCts = null;
StopIndicator();
AgentWorker.Log("RDP: Screen-Capture gestoppt");
}
}
captureCts?.Cancel();
StopIndicator();
AgentWorker.Log("RDP: Getrennt");
}
private void StopIndicator()
{
_userDisconnectCts?.Cancel();
_userDisconnectCts = null;
try { _indicatorListener?.Stop(); } catch { }
_indicatorListener = null;
if (_indicatorPid > 0)
{
try { Process.GetProcessById(_indicatorPid).Kill(); } catch { }
_indicatorPid = -1;
}
}
private static string GenerateSecret() => Convert.ToHexString(RandomNumberGenerator.GetBytes(16));
// Liest exakt secret.Length Bytes und vergleicht zeitkonstant — verhindert, dass ein beliebiger
// lokaler Prozess sich als der gespawnte Helper ausgibt und Consent/Disconnect/Frames vortäuscht.
private static async Task<bool> ValidateSecretAsync(NetworkStream stream, string secret, CancellationToken ct)
{
var expected = Encoding.ASCII.GetBytes(secret);
var buf = new byte[expected.Length];
var read = 0;
while (read < buf.Length)
{
int n;
try { n = await stream.ReadAsync(buf.AsMemory(read, buf.Length - read), ct); }
catch { return false; }
if (n == 0) return false;
read += n;
}
return CryptographicOperations.FixedTimeEquals(buf, expected);
}
private async Task ConsentAndCaptureAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
{
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
var secret = GenerateSecret();
var consentListener = new TcpListener(IPAddress.Loopback, 0);
consentListener.Start();
var consentPort = ((IPEndPoint)consentListener.LocalEndpoint).Port;
if (!SpawnConsentHelper(exePath, consentPort.ToString()))
if (!SpawnConsentHelper(exePath, consentPort.ToString(), secret))
{
consentListener.Stop();
AgentWorker.Log("RDP: Consent-Helper fehlgeschlagen, starte ohne Consent");
await CapturePipeLoopAsync(ws, screenIdx, ct);
AgentWorker.Log("RDP: Consent-Helper fehlgeschlagen, verweigere Zugriff");
var failed = Encoding.UTF8.GetBytes("{\"type\":\"rdp_denied\",\"reason\":\"consent_spawn_failed\"}");
if (ws.State == WebSocketState.Open)
await ws.SendAsync(new ArraySegment<byte>(failed), WebSocketMessageType.Text, true, CancellationToken.None);
return;
}
@@ -108,9 +162,15 @@ public class RtcService
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(35));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
using var tcp = await consentListener.AcceptTcpClientAsync(linked.Token);
var b = tcp.GetStream().ReadByte();
accepted = b == 1;
while (!linked.IsCancellationRequested)
{
using var tcp = await consentListener.AcceptTcpClientAsync(linked.Token);
var stream = tcp.GetStream();
if (!await ValidateSecretAsync(stream, secret, linked.Token)) continue; // fremder Connect-Versuch ohne gültiges Secret — ignorieren, weiter warten
var b = stream.ReadByte();
accepted = b == 1;
break;
}
}
catch { accepted = false; }
finally { consentListener.Stop(); }
@@ -125,72 +185,71 @@ public class RtcService
}
AgentWorker.Log("RDP: User hat Zugriff erlaubt, starte Capture");
await CapturePipeLoopAsync(ws, screenIdx, ct);
SpawnIndicator(exePath);
var combinedToken = _userDisconnectCts != null
? CancellationTokenSource.CreateLinkedTokenSource(ct, _userDisconnectCts.Token).Token
: ct;
await CapturePipeLoopAsync(ws, screenIdx, combinedToken);
}
private static bool SpawnConsentHelper(string exePath, string portStr)
// Zeigt dauerhaftes "Bildschirm wird übertragen"-Overlay (TeamViewer-Style) solange Capture läuft.
// User kann per Klick selbst trennen — Signal landet hier als CancellationTokenSource.Cancel().
private void SpawnIndicator(string exePath)
{
try
{
var fullUser = NotificationService.GetLoggedOnUser();
if (string.IsNullOrEmpty(fullUser))
{
AgentWorker.Log("RDP: Kein eingeloggter User für Consent");
return false;
}
var taskName = $"ITNexus-RDPConsent-{portStr}";
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss");
var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase)
? "/ru \"INTERACTIVE\""
: $"/ru \"{fullUser}\"";
var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-consent {portStr}\" " +
$"/sc ONCE /st {triggerTime} {ruArg} /it /f";
var p = Process.Start(new ProcessStartInfo("schtasks.exe", args)
{ CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false });
p?.WaitForExit();
if (p?.ExitCode != 0)
{
AgentWorker.Log($"RDP: schtasks consent fehlgeschlagen (ExitCode={p?.ExitCode})");
return false;
}
Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"")
{ CreateNoWindow = true })?.WaitForExit();
_indicatorListener = new TcpListener(IPAddress.Loopback, 0);
_indicatorListener.Start();
var port = ((IPEndPoint)_indicatorListener.LocalEndpoint).Port;
var secret = GenerateSecret();
_userDisconnectCts = new CancellationTokenSource();
var listener = _indicatorListener;
var disconnectCts = _userDisconnectCts;
_ = Task.Run(async () =>
{
await Task.Delay(40000);
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
try
{
while (true)
{
using var tcp = await listener.AcceptTcpClientAsync();
var stream = tcp.GetStream();
if (!await ValidateSecretAsync(stream, secret, CancellationToken.None)) continue; // fremder Connect-Versuch ohne gültiges Secret
stream.ReadByte();
disconnectCts.Cancel();
AgentWorker.Log("RDP: User hat über Overlay getrennt");
break;
}
}
catch { }
});
AgentWorker.Log($"RDP: Consent-Helper gestartet als '{fullUser}'");
return true;
_indicatorPid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-indicator {port} {secret}");
}
catch (Exception ex)
{
AgentWorker.Log($"RDP: SpawnConsentHelper Fehler: {ex.Message}");
return false;
AgentWorker.Log($"RDP: Indicator-Start fehlgeschlagen: {ex.Message}");
}
}
private static bool SpawnConsentHelper(string exePath, string portStr, string secret)
{
var pid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-consent {portStr} {secret}");
return pid > 0;
}
private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
{
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
var secret = GenerateSecret();
// TCP Loopback: kein ACL-Problem zwischen SYSTEM-Service und User-Prozess
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
if (!SpawnCaptureHelper(exePath, port.ToString(), screenIdx))
if (!SpawnCaptureHelper(exePath, port.ToString(), screenIdx, secret))
{
listener.Stop();
AgentWorker.Log("RDP: Helper-Start fehlgeschlagen");
@@ -202,7 +261,16 @@ public class RtcService
{
using var connectCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, connectCts.Token);
tcp = await listener.AcceptTcpClientAsync(linked.Token);
while (true)
{
var candidate = await listener.AcceptTcpClientAsync(linked.Token);
if (await ValidateSecretAsync(candidate.GetStream(), secret, linked.Token))
{
tcp = candidate;
break;
}
candidate.Dispose(); // fremder Connect-Versuch ohne gültiges Secret — verwerfen, weiter warten
}
}
catch
{
@@ -260,58 +328,9 @@ public class RtcService
AgentWorker.Log("RDP: Frame-Loop beendet");
}
private static bool SpawnCaptureHelper(string exePath, string portStr, int screenIdx = 0)
private static bool SpawnCaptureHelper(string exePath, string portStr, int screenIdx, string secret)
{
try
{
var fullUser = NotificationService.GetLoggedOnUser();
if (string.IsNullOrEmpty(fullUser))
{
AgentWorker.Log("RDP: Kein eingeloggter User gefunden");
return false;
}
var taskName = $"ITNexus-RDP-{portStr}";
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss");
var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase)
? "/ru \"INTERACTIVE\""
: $"/ru \"{fullUser}\"";
var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-capture {portStr} {screenIdx}\" " +
$"/sc ONCE /st {triggerTime} {ruArg} /it /f";
var p = Process.Start(new ProcessStartInfo("schtasks.exe", args)
{ CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false });
p?.WaitForExit();
if (p?.ExitCode != 0)
{
AgentWorker.Log($"RDP: schtasks create fehlgeschlagen (ExitCode={p?.ExitCode})");
return false;
}
Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"")
{ CreateNoWindow = true })?.WaitForExit();
// Aufräumen nach kurzer Zeit
_ = Task.Run(async () =>
{
await Task.Delay(5000);
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
});
AgentWorker.Log($"RDP: Helper gestartet als '{fullUser}' (Task: {taskName})");
return true;
}
catch (Exception ex)
{
AgentWorker.Log($"RDP: SpawnHelper Fehler: {ex.Message}");
return false;
}
var pid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-capture {portStr} {screenIdx} {secret}");
return pid > 0;
}
}

View File

@@ -0,0 +1,160 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ITNexusAgent.Services;
// Startet Prozesse in der aktiven User-Session (aus SYSTEM-Service heraus)
// Korrekte Win32-Methode: WTSQueryUserToken + CreateProcessAsUser
public static class SessionSpawner
{
#region Win32 P/Invoke
[DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId();
[DllImport("Wtsapi32.dll", SetLastError = true)]
static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken);
[DllImport("Wtsapi32.dll", SetLastError = true)]
static extern bool WTSEnumerateSessions(IntPtr hServer, uint reserved, uint version,
out IntPtr ppSessionInfo, out uint pCount);
[DllImport("Wtsapi32.dll")] static extern void WTSFreeMemory(IntPtr pMemory);
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess,
IntPtr lpTokenAttributes, int impersonationLevel, int tokenType, out IntPtr phNewToken);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool CreateProcessAsUser(IntPtr hToken, string? lpApplicationName,
string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,
bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment,
string? lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("userenv.dll", SetLastError = true)]
static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);
[DllImport("userenv.dll", SetLastError = true)]
static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr hObject);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct STARTUPINFO
{
public int cb; public string? lpReserved; public string? lpDesktop; public string? lpTitle;
public uint dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags;
public ushort wShowWindow, cbReserved2; public IntPtr lpReserved2;
public IntPtr hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
struct PROCESS_INFORMATION
{
public IntPtr hProcess, hThread;
public uint dwProcessId, dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
struct WTS_SESSION_INFO
{
public uint SessionId; [MarshalAs(UnmanagedType.LPStr)] public string? pWinStationName;
public int State; // 0=Active
}
#endregion
// Spawnt Prozess als eingeloggter Desktop-User. Gibt PID zurück oder -1 bei Fehler.
public static int SpawnInUserSession(string exePath, string args)
{
var sessionId = FindActiveUserSession();
if (sessionId == uint.MaxValue)
{
AgentWorker.Log("SessionSpawner: Keine aktive User-Session gefunden");
return -1;
}
if (!WTSQueryUserToken(sessionId, out var userToken))
{
AgentWorker.Log($"SessionSpawner: WTSQueryUserToken fehlgeschlagen (Session={sessionId}, Error={Marshal.GetLastWin32Error()})");
return -1;
}
try
{
if (!DuplicateTokenEx(userToken, 0x10000000u, IntPtr.Zero, 2, 1, out var dupToken))
{
AgentWorker.Log($"SessionSpawner: DuplicateTokenEx fehlgeschlagen (Error={Marshal.GetLastWin32Error()})");
return -1;
}
try
{
CreateEnvironmentBlock(out var envBlock, dupToken, false);
var si = new STARTUPINFO
{
cb = Marshal.SizeOf<STARTUPINFO>(),
lpDesktop = "winsta0\\default",
dwFlags = 1, // STARTF_USESHOWWINDOW
wShowWindow = 1 // SW_SHOWNORMAL
};
var cmdLine = $"\"{exePath}\" {args}";
bool ok = CreateProcessAsUser(dupToken, null, cmdLine,
IntPtr.Zero, IntPtr.Zero, false,
0x0400, // CREATE_UNICODE_ENVIRONMENT
envBlock, null, ref si, out var pi);
if (envBlock != IntPtr.Zero) DestroyEnvironmentBlock(envBlock);
if (!ok)
{
AgentWorker.Log($"SessionSpawner: CreateProcessAsUser fehlgeschlagen (Error={Marshal.GetLastWin32Error()})");
return -1;
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
AgentWorker.Log($"SessionSpawner: Prozess gestartet (PID={pi.dwProcessId}, Session={sessionId})");
return (int)pi.dwProcessId;
}
finally { CloseHandle(dupToken); }
}
finally { CloseHandle(userToken); }
}
private static uint FindActiveUserSession()
{
// Zuerst Console-Session versuchen
var consoleSession = WTSGetActiveConsoleSessionId();
if (consoleSession != uint.MaxValue && TryGetTokenForSession(consoleSession))
return consoleSession;
// Alle Sessions durchsuchen → erste aktive (State=0) nehmen
if (!WTSEnumerateSessions(IntPtr.Zero, 0, 1, out var pInfo, out var count))
return uint.MaxValue;
try
{
var size = Marshal.SizeOf<WTS_SESSION_INFO>();
for (int i = 0; i < count; i++)
{
var info = Marshal.PtrToStructure<WTS_SESSION_INFO>(IntPtr.Add(pInfo, i * size));
if (info.State == 0 && info.SessionId != 0) // Active, nicht Session 0
return info.SessionId;
}
}
finally { WTSFreeMemory(pInfo); }
return uint.MaxValue;
}
private static bool TryGetTokenForSession(uint sessionId)
{
if (!WTSQueryUserToken(sessionId, out var tok)) return false;
CloseHandle(tok);
return true;
}
}

View File

@@ -40,7 +40,7 @@ public class ShellService
+ $"/ws?type=agent&key={Uri.EscapeDataString(_agentKey)}&hostname={Uri.EscapeDataString(_hostname)}";
using var ws = new ClientWebSocket();
ws.Options.SetRequestHeader("User-Agent", "IT-Nexus-Agent/2.1.2");
ws.Options.SetRequestHeader("User-Agent", "IT-Nexus-Agent/2.3.0");
await ws.ConnectAsync(new Uri(wsUrl), ct);
AgentWorker.Log("SHELL: WebSocket verbunden");

View File

@@ -0,0 +1,57 @@
<Window x:Class="ITNexusAgent.UI.RdpActiveIndicatorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IT Nexus Bildschirmübertragung aktiv"
Width="300" Height="64"
WindowStartupLocation="Manual"
ResizeMode="NoResize"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="True"
ShowInTaskbar="False"
FontFamily="Segoe UI">
<Border Background="#1A1D2E" BorderBrush="#E67E22" BorderThickness="1.5" CornerRadius="10">
<Grid Margin="14,10,14,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Pulsierender roter Punkt -->
<Ellipse x:Name="PulseDot" Grid.Column="0" Width="10" Height="10" Fill="#E74C3C"
VerticalAlignment="Center" Margin="0,0,12,0">
<Ellipse.Triggers>
</Ellipse.Triggers>
</Ellipse>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="Bildschirm wird übertragen" FontSize="12" FontWeight="Bold" Foreground="White"/>
<TextBlock x:Name="DurationText" Text="IT-Abteilung sieht zu · 00:00" FontSize="10.5" Foreground="#9099B5" Margin="0,2,0,0"/>
</StackPanel>
<Button x:Name="DisconnectButton" Grid.Column="2" Click="DisconnectButton_Click"
Background="#3D1414" Foreground="#FF8A80" BorderThickness="1" BorderBrush="#6B2020"
Padding="10,6" Cursor="Hand" VerticalAlignment="Center" FontSize="11" FontWeight="Bold">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="6" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<TextBlock Text="Trennen"/>
</Button>
</Grid>
</Border>
<Window.Resources>
<Storyboard x:Key="PulseAnim" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="PulseDot" Storyboard.TargetProperty="Opacity"
From="1.0" To="0.25" Duration="0:0:0.9" AutoReverse="True"/>
</Storyboard>
</Window.Resources>
</Window>

View File

@@ -0,0 +1,60 @@
using System.Net.Sockets;
using System.Text;
using System.Windows;
using System.Windows.Media.Animation;
namespace ITNexusAgent.UI;
public partial class RdpActiveIndicatorWindow : Window
{
private readonly int _port;
private readonly string _secret;
private readonly DateTime _startedAt = DateTime.Now;
private readonly System.Windows.Threading.DispatcherTimer _timer = new();
private bool _signaled = false;
public RdpActiveIndicatorWindow(int port, string secret)
{
InitializeComponent();
_port = port;
_secret = secret;
Loaded += (s, e) =>
{
var area = SystemParameters.WorkArea;
Left = area.Right - Width - 16;
Top = area.Bottom - Height - 16;
((Storyboard)Resources["PulseAnim"]).Begin(PulseDot);
};
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (s, e) =>
{
var elapsed = DateTime.Now - _startedAt;
DurationText.Text = $"IT-Abteilung sieht zu · {elapsed:mm\\:ss}";
};
_timer.Start();
}
private void DisconnectButton_Click(object sender, RoutedEventArgs e)
{
SendDisconnectSignal();
Close();
}
private void SendDisconnectSignal()
{
if (_signaled) return;
_signaled = true;
try
{
using var tcp = new TcpClient();
tcp.Connect("127.0.0.1", _port);
var stream = tcp.GetStream();
stream.Write(Encoding.ASCII.GetBytes(_secret));
stream.WriteByte(1);
stream.Flush();
}
catch { }
}
}

View File

@@ -1,15 +1,16 @@
<Window x:Class="ITNexusAgent.UI.RdpConsentWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IT Nexus Bildschirmzugriff"
Title="IT Nexus Bildschirmzugriff angefordert"
Width="480" Height="Auto"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
WindowStyle="None"
WindowStyle="ToolWindow"
AllowsTransparency="False"
Background="#1A1D2E"
Topmost="True"
ShowInTaskbar="True"
FontFamily="Segoe UI">
<Window.Resources>

View File

@@ -1,4 +1,5 @@
using System.Net.Sockets;
using System.Text;
using System.Windows;
namespace ITNexusAgent.UI;
@@ -6,13 +7,15 @@ namespace ITNexusAgent.UI;
public partial class RdpConsentWindow : Window
{
private readonly int _port;
private readonly string _secret;
private bool _answered = false;
private System.Threading.CancellationTokenSource _countdownCts = new();
public RdpConsentWindow(int port)
public RdpConsentWindow(int port, string secret)
{
InitializeComponent();
_port = port;
_secret = secret;
_ = RunCountdownAsync(_countdownCts.Token);
}
@@ -53,8 +56,10 @@ public partial class RdpConsentWindow : Window
{
using var tcp = new TcpClient();
tcp.Connect("127.0.0.1", _port);
tcp.GetStream().WriteByte((byte)(accepted ? 1 : 0));
tcp.GetStream().Flush();
var stream = tcp.GetStream();
stream.Write(Encoding.ASCII.GetBytes(_secret));
stream.WriteByte((byte)(accepted ? 1 : 0));
stream.Flush();
}
catch { }
}

View File

@@ -1,5 +1,5 @@
#define MyAppName "IT Nexus Agent"
#define MyAppVersion "2.2.0"
#define MyAppVersion "2.9.0"
#define MyAppPublisher "Cereda Systems GmbH"
#define MyAppURL "https://it-nexus.cereda-systems.de"
#define MyAppExeName "IT-Nexus-Agent.exe"
@@ -64,17 +64,31 @@ Type: files; Name: "{commonappdata}\IT Nexus Agent\agent.log"
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
AppData: String;
begin
if CurStep = ssInstall then begin
// Service stoppen (neue Architektur)
Exec('net.exe', 'stop "IT Nexus Agent"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
// Alte Scheduled Tasks (v1.x PowerShell-Agent) entfernen
Sleep(1500);
// Verwaiste Prozesse hart killen (überlebt Service-Stop manchmal, z.B. Consent/Capture-Helper)
Exec('taskkill.exe', '/F /IM IT-Nexus-Agent.exe /T', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Sleep(1000);
// Reste eines laufenden Self-Update-Zyklus entfernen (update.cmd + heruntergeladener Installer)
AppData := ExpandConstant('{commonappdata}\IT Nexus Agent');
if FileExists(AppData + '\update.cmd') then
DeleteFile(AppData + '\update.cmd');
if FileExists(AppData + '\IT-Nexus-Agent-Setup-Update.exe') then
DeleteFile(AppData + '\IT-Nexus-Agent-Setup-Update.exe');
// Alte Scheduled Tasks (v1.x PowerShell-Agent + RDP-Helper-Reste) entfernen
Exec('schtasks.exe', '/delete /tn "IT Nexus Agent" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('schtasks.exe', '/delete /tn "IT Nexus Agent Watcher" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('schtasks.exe', '/delete /tn "ITNexusAgent" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('schtasks.exe', '/delete /tn "ITNexusAgentWatcher" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
// Autostart-Eintrag aus vorheriger Version entfernen (falls vorhanden)
RegDeleteValue(HKCU, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'IT Nexus Agent');
Sleep(2000);
Sleep(1500);
end;
end;

View File

@@ -1,524 +0,0 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="x-ua-compatible" content="ie=11">
<title>IT Nexus Agent Setup</title>
<HTA:APPLICATION
APPLICATIONNAME="IT Nexus Agent Setup"
BORDER="thin"
BORDERSTYLE="normal"
CAPTION="yes"
CONTEXTMENU="no"
MAXIMIZEBUTTON="no"
MINIMIZEBUTTON="yes"
NAVIGABLE="no"
SCROLL="no"
SELECTION="no"
SHOWINTASKBAR="yes"
SINGLEINSTANCE="yes"
SYSMENU="yes"
WINDOWSTATE="normal"
/>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Segoe UI', Tahoma, sans-serif;
background: #1e1e2e;
color: #cdd6f4;
width: 100%;
height: 100%;
overflow: hidden;
}
/* Sidebar */
.sidebar {
position: absolute;
left: 0; top: 0; bottom: 0;
width: 220px;
background: #181825;
border-right: 1px solid #313244;
display: flex;
flex-direction: column;
padding: 28px 0 20px;
}
.sidebar-logo {
padding: 0 20px 28px;
border-bottom: 1px solid #313244;
margin-bottom: 20px;
}
.sidebar-logo-title {
font-size: 20px;
font-weight: 700;
color: #cdd6f4;
line-height: 1.2;
}
.sidebar-logo-sub {
font-size: 14px;
color: #6c7086;
margin-top: 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 20px;
font-size: 16px;
color: #6c7086;
cursor: default;
}
.nav-item.done { color: #a6e3a1; }
.nav-item.active { color: #89b4fa; font-weight: 600; background: rgba(137,180,250,0.08); border-right: 3px solid #89b4fa; }
.nav-num {
width: 28px; height: 28px;
border-radius: 50%;
font-size: 13px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
background: #313244;
color: #6c7086;
}
.nav-item.done .nav-num { background: #a6e3a1; color: #1e1e2e; }
.nav-item.active .nav-num { background: #89b4fa; color: #1e1e2e; }
/* Main */
.main {
position: absolute;
left: 220px; top: 0; right: 0; bottom: 60px;
padding: 36px 36px 16px;
overflow: hidden;
}
.step { display: none; }
.step.active { display: block; }
.step-title {
font-size: 28px;
font-weight: 700;
color: #cdd6f4;
margin-bottom: 10px;
}
.step-subtitle {
font-size: 16px;
color: #6c7086;
margin-bottom: 28px;
line-height: 1.6;
}
/* Feature list */
.feature-list { list-style: none; }
.feature-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid #313244;
font-size: 17px;
}
.feature-item:last-child { border-bottom: none; }
.feature-dot {
width: 10px; height: 10px;
border-radius: 50%;
background: #89b4fa;
flex-shrink: 0;
}
/* Fields */
.field { margin-bottom: 14px; }
.field label {
font-size: 14px;
color: #6c7086;
display: block;
margin-bottom: 7px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.field input {
width: 100%;
padding: 12px 14px;
background: #181825;
border: 1px solid #313244;
border-radius: 6px;
color: #cdd6f4;
font-size: 16px;
font-family: 'Segoe UI', sans-serif;
transition: border-color .15s;
}
.field input:focus { outline: none; border-color: #89b4fa; }
.field input[readonly] { color: #6c7086; }
/* Summary */
.summary-box {
background: #181825;
border: 1px solid #313244;
border-radius: 8px;
overflow: hidden;
margin-bottom: 14px;
}
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
font-size: 15px;
border-bottom: 1px solid #313244;
}
.summary-row:last-child { border-bottom: none; }
.summary-key { color: #6c7086; }
.summary-val { color: #89b4fa; font-weight: 600; font-size: 12px; max-width: 200px; text-align: right; }
.warn-box {
background: rgba(250,179,135,0.08);
border: 1px solid rgba(250,179,135,0.2);
border-radius: 6px;
padding: 10px 12px;
font-size: 12px;
color: #fab387;
}
/* Log */
#log {
background: #11111b;
border: 1px solid #313244;
border-radius: 6px;
padding: 10px 12px;
height: 50%;
overflow-y: auto;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
.log-ok { color: #a6e3a1; }
.log-err { color: #f38ba8; }
.log-info { color: #6c7086; }
.log-warn { color: #f9e2af; }
/* Progress */
.progress-label {
font-size: 12px;
color: #6c7086;
margin-bottom: 6px;
}
.progress-wrap {
background: #313244;
border-radius: 4px;
height: 6px;
margin-bottom: 12px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, #89b4fa, #b4befe);
width: 0%;
transition: width .35s ease;
}
/* Success */
.success-wrap { text-align: center; padding-top: 10px; }
.success-check {
width: 64px; height: 64px;
border-radius: 50%;
background: rgba(166,227,161,0.12);
border: 2px solid #a6e3a1;
margin: 0 auto 16px;
display: flex; align-items: center; justify-content: center;
font-size: 28px;
color: #a6e3a1;
}
.success-title {
font-size: 18px;
font-weight: 700;
color: #a6e3a1;
margin-bottom: 8px;
}
.success-sub {
font-size: 13px;
color: #6c7086;
line-height: 1.6;
}
.success-hint {
margin-top: 14px;
background: rgba(137,180,250,0.08);
border: 1px solid rgba(137,180,250,0.2);
border-radius: 6px;
padding: 10px 14px;
font-size: 12px;
color: #89b4fa;
}
/* Footer */
.footer {
position: absolute;
left: 220px; right: 0; bottom: 0;
height: 60px;
border-top: 1px solid #313244;
background: #181825;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 24px;
gap: 10px;
}
.btn {
padding: 11px 28px;
border-radius: 6px;
border: none;
font-size: 15px;
font-family: 'Segoe UI', sans-serif;
font-weight: 600;
cursor: pointer;
transition: all .15s;
min-width: 110px;
}
.btn:hover { filter: brightness(1.1); }
.btn:disabled { opacity: .35; cursor: not-allowed; }
.btn-ghost { background: transparent; color: #6c7086; border: 1px solid #313244; }
.btn-primary { background: #89b4fa; color: #1e1e2e; }
.btn-success { background: #a6e3a1; color: #1e1e2e; }
.btn-close { background: #585b70; color: #cdd6f4; }
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-logo">
<div class="sidebar-logo-title">IT Nexus Agent</div>
<div class="sidebar-logo-sub">Setup 1.0.0</div>
</div>
<div class="nav-item active" id="nav1"><div class="nav-num" id="nn1">1</div> Willkommen</div>
<div class="nav-item" id="nav2"><div class="nav-num" id="nn2">2</div> Konfiguration</div>
<div class="nav-item" id="nav3"><div class="nav-num" id="nn3">3</div> Zusammenfassung</div>
<div class="nav-item" id="nav4"><div class="nav-num" id="nn4">4</div> Installation</div>
<div class="nav-item" id="nav5"><div class="nav-num" id="nn5">5</div> Abgeschlossen</div>
</div>
<!-- Content -->
<div class="main">
<!-- Step 1 -->
<div class="step active" id="step1">
<div class="step-title">Willkommen</div>
<div class="step-subtitle">Der IT Nexus Agent wird als Windows-Dienst installiert und sendet alle 5 Minuten Systemdaten an das IT Nexus Dashboard.</div>
<ul class="feature-list">
<li class="feature-item"><span class="feature-dot"></span> CPU, RAM &amp; Festplatten-Auslastung</li>
<li class="feature-item"><span class="feature-dot"></span> Installierte Software</li>
<li class="feature-item"><span class="feature-dot"></span> Windows Update Status</li>
<li class="feature-item"><span class="feature-dot"></span> Angemeldeter Benutzer &amp; Uptime</li>
<li class="feature-item"><span class="feature-dot"></span> Netzwerk &amp; Systeminformationen</li>
</ul>
</div>
<!-- Step 2 -->
<div class="step" id="step2">
<div class="step-title">Konfiguration</div>
<div class="step-subtitle">Serververbindung konfigurieren.</div>
<div class="field">
<label>Server URL</label>
<input type="text" id="serverUrl" value="https://it-nexus.cereda-systems.de">
</div>
<div class="field">
<label>Agent API Key</label>
<input type="text" id="agentKey" value="cereda-agent-2024-secure-key">
</div>
<div class="field">
<label>Installationsverzeichnis</label>
<input type="text" id="installDir" value="C:\ProgramData\IT Nexus Agent" readonly>
</div>
</div>
<!-- Step 3 -->
<div class="step" id="step3">
<div class="step-title">Bereit zur Installation</div>
<div class="step-subtitle">Bitte alles pruefen, dann auf Installieren klicken.</div>
<div class="summary-box">
<div class="summary-row"><span class="summary-key">Verzeichnis</span><span class="summary-val" id="sum-dir"></span></div>
<div class="summary-row"><span class="summary-key">Server</span><span class="summary-val" id="sum-url"></span></div>
<div class="summary-row"><span class="summary-key">Scheduled Task</span><span class="summary-val">Alle 5 Minuten (SYSTEM)</span></div>
<div class="summary-row"><span class="summary-key">Agent Version</span><span class="summary-val">1.0.0</span></div>
</div>
<div class="warn-box">Hinweis: Es erscheint eine UAC-Abfrage fuer Administrator-Rechte.</div>
</div>
<!-- Step 4 -->
<div class="step" id="step4">
<div class="step-title">Installation</div>
<div class="progress-label" id="progLabel">Vorbereitung...</div>
<div class="progress-wrap"><div class="progress-fill" id="progBar"></div></div>
<div id="log"></div>
</div>
<!-- Step 5 -->
<div class="step" id="step5">
<div class="success-wrap">
<div class="success-check">&#10003;</div>
<div class="success-title">Installation abgeschlossen</div>
<div class="success-sub">Der IT Nexus Agent ist installiert und aktiv.<br>Er sendet alle 5 Minuten Daten an das Dashboard.</div>
<div class="success-hint">Sichtbar unter: IT Nexus &#8594; Administration &#8594; Agent Monitoring</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<button class="btn btn-ghost" id="btnBack" onclick="prevStep()" style="display:none">Zurueck</button>
<button class="btn btn-ghost" id="btnCancel" onclick="window.close()">Abbrechen</button>
<button class="btn btn-primary" id="btnNext" onclick="nextStep()">Weiter</button>
</div>
<script language="JScript">
var step = 1;
var shell = new ActiveXObject("WScript.Shell");
var fso = new ActiveXObject("Scripting.FileSystemObject");
// Groesse anhand Bildschirmaufloesung berechnen (60% Breite, 70% Hoehe, min/max begrenzt)
var w = Math.min(Math.max(Math.round(screen.availWidth * 0.60), 700), 1100);
var h = Math.min(Math.max(Math.round(screen.availHeight * 0.70), 560), 820);
window.resizeTo(w, h);
window.moveTo((screen.availWidth-w)/2, (screen.availHeight-h)/2);
// Schriftgroesse skalieren
var scale = w / 780;
document.body.style.fontSize = Math.round(13 * scale) + "px";
function updateNav() {
for (var i=1;i<=5;i++) {
var n = document.getElementById("nav"+i);
var nn = document.getElementById("nn"+i);
if (i < step) { n.className="nav-item done"; nn.innerHTML="&#10003;"; }
else if (i === step) { n.className="nav-item active"; nn.innerHTML=i; }
else { n.className="nav-item"; nn.innerHTML=i; }
}
}
function showStep(n) {
for (var i=1;i<=5;i++) {
var el=document.getElementById("step"+i);
if(el) el.className=(i===n)?"step active":"step";
}
step=n;
updateNav();
var btnBack = document.getElementById("btnBack");
var btnCancel = document.getElementById("btnCancel");
var btnNext = document.getElementById("btnNext");
btnBack.style.display = (n>1 && n<4) ? "" : "none";
btnCancel.style.display = (n>=4) ? "none" : "";
if (n===3) {
document.getElementById("sum-dir").innerText = document.getElementById("installDir").value;
document.getElementById("sum-url").innerText = document.getElementById("serverUrl").value;
btnNext.className = "btn btn-success";
btnNext.innerText = "Installieren";
} else if (n===4) {
btnNext.style.display="none";
startInstall();
} else if (n===5) {
btnNext.className="btn btn-close";
btnNext.innerText="Schliessen";
btnNext.style.display="";
} else {
btnNext.className="btn btn-primary";
btnNext.innerText="Weiter";
btnNext.style.display="";
}
}
function nextStep() { if(step===5){window.close();return;} showStep(step+1); }
function prevStep() { if(step>1) showStep(step-1); }
function log(msg, type) {
var el=document.getElementById("log");
el.innerHTML+='<div class="'+(type||"log-info")+'">'+msg+'</div>';
el.scrollTop=el.scrollHeight;
}
function setProgress(pct, label) {
document.getElementById("progBar").style.width=pct+"%";
if(label) document.getElementById("progLabel").innerText=label;
}
function sleep(ms) { var s=new Date().getTime(); while(new Date().getTime()-s<ms){} }
function writeFile(path, content) {
var f=fso.CreateTextFile(path,true,false);
f.Write(content); f.Close();
}
function startInstall() {
try {
var installDir = document.getElementById("installDir").value;
var serverUrl = document.getElementById("serverUrl").value;
var agentKey = document.getElementById("agentKey").value;
setProgress(10, "Erstelle Verzeichnis...");
sleep(300);
if (!fso.FolderExists(installDir)) fso.CreateFolder(installDir);
log("Verzeichnis erstellt: " + installDir, "log-ok");
setProgress(25, "Kopiere Agent-Dateien...");
sleep(300);
var htaDir = fso.GetParentFolderName(location.href.replace("file:///","").replace(/\//g,"\\"));
try { fso.CopyFile(htaDir+"\\it-nexus-agent.ps1", installDir+"\\it-nexus-agent.ps1", true); }
catch(e) { log("Agent-Script wird neu erstellt...", "log-warn"); }
log("Agent-Script kopiert", "log-ok");
setProgress(40, "Schreibe Konfiguration...");
sleep(200);
writeFile(installDir+"\\config.json", '{\n "server_url": "'+serverUrl+'",\n "agent_key": "'+agentKey+'"\n}');
log("Konfiguration gespeichert", "log-ok");
setProgress(60, "Registriere Scheduled Task...");
sleep(300);
var ps = [
'$d = "'+installDir+'"',
'$a = New-ScheduledTaskAction -Execute "powershell.exe" -Argument ("-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File `\\"$d\\it-nexus-agent.ps1`\\"")',
'$t = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 5) -Once -At (Get-Date)',
'$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -MultipleInstances IgnoreNew -StartWhenAvailable',
'$p = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest',
'Unregister-ScheduledTask -TaskName "IT Nexus Agent" -Confirm:$false -ErrorAction SilentlyContinue',
'Register-ScheduledTask -TaskName "IT Nexus Agent" -Action $a -Trigger $t -Settings $s -Principal $p -Force | Out-Null',
'Start-ScheduledTask -TaskName "IT Nexus Agent"'
].join("\r\n");
writeFile(installDir+"\\setup-task.ps1", ps);
// Als Administrator ausfuehren (UAC-Prompt)
var shellApp = new ActiveXObject("Shell.Application");
shellApp.ShellExecute(
"powershell.exe",
"-ExecutionPolicy Bypass -File \"" + installDir + "\\setup-task.ps1\"",
"",
"runas",
1
);
// Warten bis Task registriert ist
sleep(4000);
setProgress(85, "Pruefe Installation...");
sleep(600);
log("Scheduled Task registriert (alle 5 Minuten)", "log-ok");
log("Erster Checkin wird ausgefuehrt...", "log-info");
sleep(400);
log("Agent ist aktiv!", "log-ok");
setProgress(100, "Abgeschlossen");
sleep(800);
showStep(5);
} catch(e) {
log("Fehler: " + e.message, "log-err");
setProgress(0, "Fehler aufgetreten");
var btn=document.getElementById("btnNext");
btn.innerText="Schliessen"; btn.className="btn btn-ghost"; btn.style.display="";
}
}
</script>
</body>
</html>

View File

@@ -1,74 +0,0 @@
# IT Nexus Announcement Watcher
# Wird alle 15 Sekunden als SYSTEM ausgefuehrt
# Zeigt Ankuendigungen als Desktop-Popup fuer den eingeloggten Benutzer
param()
$cfg = Get-Content 'C:\ProgramData\IT Nexus Agent\config.json' -Raw | ConvertFrom-Json
$url = $cfg.server_url.TrimEnd('/')
$key = $cfg.agent_key
function Show-AnnDialog($ann) {
$id = $ann.id
$dlg = "C:\ProgramData\IT Nexus Agent\ann_$id.ps1"
if (Test-Path $dlg) { return }
$title = ($ann.title -replace "'", "''")
$message = ($ann.message -replace "'", "''")
$accent = switch ($ann.type) { 'warning'{'220,50,50'} 'maintenance'{'245,158,11'} default{'99,102,241'} }
$label = switch ($ann.type) { 'warning'{'WICHTIGE WARNUNG'} 'maintenance'{'WARTUNGSANKUENDIGUNG'} default{'INFORMATION'} }
$ackUrl = "$url/api/announcements/$id/ack-agent"
$n = [System.Environment]::NewLine
$headerColor = switch ($ann.type) { 'warning'{'220,53,69'} 'maintenance'{'255,140,0'} default{'0,120,212'} }
$iconText = switch ($ann.type) { 'warning'{'⚠'} 'maintenance'{'🔧'} default{''} }
$code = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing$n"
# Hauptfenster - weißer Hintergrund, professionell
$code += "`$f=New-Object System.Windows.Forms.Form;`$f.Text='IT Nexus Mitteilung';`$f.Size=New-Object System.Drawing.Size(480,320);`$f.StartPosition='CenterScreen';`$f.FormBorderStyle='FixedSingle';`$f.MaximizeBox=`$false;`$f.MinimizeBox=`$false;`$f.TopMost=`$true;`$f.BackColor=[System.Drawing.Color]::White$n"
# Farbiger Header-Balken
$code += "`$hdr=New-Object System.Windows.Forms.Panel;`$hdr.Size=New-Object System.Drawing.Size(480,56);`$hdr.Location=New-Object System.Drawing.Point(0,0);`$hdr.BackColor=[System.Drawing.Color]::FromArgb($headerColor);`$f.Controls.Add(`$hdr)$n"
# Logo-Text links im Header
$code += "`$logo=New-Object System.Windows.Forms.Label;`$logo.Text='IT Nexus';`$logo.Font=New-Object System.Drawing.Font('Segoe UI',11,[System.Drawing.FontStyle]::Bold);`$logo.ForeColor=[System.Drawing.Color]::White;`$logo.Location=New-Object System.Drawing.Point(16,8);`$logo.Size=New-Object System.Drawing.Size(120,20);`$hdr.Controls.Add(`$logo)$n"
# Typ-Label im Header
$code += "`$lbl=New-Object System.Windows.Forms.Label;`$lbl.Text='$label';`$lbl.Font=New-Object System.Drawing.Font('Segoe UI',8);`$lbl.ForeColor=[System.Drawing.Color]::FromArgb(220,240,255);`$lbl.Location=New-Object System.Drawing.Point(16,30);`$lbl.Size=New-Object System.Drawing.Size(420,18);`$hdr.Controls.Add(`$lbl)$n"
# Titel
$code += "`$lt=New-Object System.Windows.Forms.Label;`$lt.Text='$title';`$lt.Font=New-Object System.Drawing.Font('Segoe UI',12,[System.Drawing.FontStyle]::Bold);`$lt.ForeColor=[System.Drawing.Color]::FromArgb(30,30,30);`$lt.Location=New-Object System.Drawing.Point(16,72);`$lt.Size=New-Object System.Drawing.Size(444,28);`$f.Controls.Add(`$lt)$n"
# Trennlinie
$code += "`$sep=New-Object System.Windows.Forms.Panel;`$sep.Size=New-Object System.Drawing.Size(448,1);`$sep.Location=New-Object System.Drawing.Point(16,104);`$sep.BackColor=[System.Drawing.Color]::FromArgb(220,220,220);`$f.Controls.Add(`$sep)$n"
# Nachricht
$code += "`$lm=New-Object System.Windows.Forms.Label;`$lm.Text='$message';`$lm.Font=New-Object System.Drawing.Font('Segoe UI',10);`$lm.ForeColor=[System.Drawing.Color]::FromArgb(60,60,60);`$lm.Location=New-Object System.Drawing.Point(16,112);`$lm.Size=New-Object System.Drawing.Size(444,120);`$lm.AutoSize=`$false;`$f.Controls.Add(`$lm)$n"
# Button
$code += "`$btn=New-Object System.Windows.Forms.Button;`$btn.Text='✓ Gelesen und bestätigt';`$btn.Font=New-Object System.Drawing.Font('Segoe UI',10,[System.Drawing.FontStyle]::Bold);`$btn.ForeColor=[System.Drawing.Color]::White;`$btn.BackColor=[System.Drawing.Color]::FromArgb($headerColor);`$btn.FlatStyle='Flat';`$btn.FlatAppearance.BorderSize=0;`$btn.Location=New-Object System.Drawing.Point(16,244);`$btn.Size=New-Object System.Drawing.Size(444,36);`$btn.DialogResult=[System.Windows.Forms.DialogResult]::OK;`$f.Controls.Add(`$btn);`$f.AcceptButton=`$btn$n"
$code += "if(`$f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){$n"
$code += " `$b=[System.Text.Encoding]::UTF8.GetBytes('{""hostname"":""+`$env:COMPUTERNAME+'""}')$n"
$code += " `$r=[System.Net.WebRequest]::Create('$ackUrl')$n"
$code += " `$r.Method='POST';`$r.ContentType='application/json';`$r.Headers.Add('X-Agent-Key','$key');`$r.ContentLength=`$b.Length$n"
$code += " `$s=`$r.GetRequestStream();`$s.Write(`$b,0,`$b.Length);`$s.Close()$n"
$code += " try{`$r.GetResponse().Close()}catch{}$n"
$code += " Remove-Item '$dlg' -Force -ErrorAction SilentlyContinue$n"
$code += "}$n"
$code | Out-File -FilePath $dlg -Encoding UTF8 -Force
$loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName
if ($loggedUser) {
$tn = "ITNexus-AnnDlg-$id"
Unregister-ScheduledTask -TaskName $tn -Confirm:$false -ErrorAction SilentlyContinue
$a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$dlg`""
$t = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(2)
$p = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive -RunLevel Limited
$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -StartWhenAvailable
Register-ScheduledTask -TaskName $tn -Action $a -Trigger $t -Principal $p -Settings $s -Force | Out-Null
}
}
try {
$body = [System.Text.Encoding]::UTF8.GetBytes('{"hostname":"'+$env:COMPUTERNAME+'"}')
$req = [System.Net.WebRequest]::Create("$url/api/monitoring/announcements-poll")
$req.Method = 'POST'; $req.ContentType = 'application/json'
$req.Headers.Add('X-Agent-Key', $key); $req.ContentLength = $body.Length
$st = $req.GetRequestStream(); $st.Write($body, 0, $body.Length); $st.Close()
$rd = New-Object System.IO.StreamReader($req.GetResponse().GetResponseStream())
$data = $rd.ReadToEnd() | ConvertFrom-Json
if ($data.announcements) { foreach ($a in $data.announcements) { Show-AnnDialog $a } }
} catch {}

View File

@@ -1,4 +0,0 @@
{
"server_url": "https://it-nexus.cereda-systems.de",
"agent_key": "DEIN-AGENT-KEY-HIER"
}

View File

@@ -1,4 +0,0 @@
{
"server_url": "https://it-nexus.cereda-systems.de",
"agent_key": "cereda-agent-2024-secure-key"
}

View File

@@ -1,44 +0,0 @@
# IT Nexus Agent - Installer
# Cereda Systems GmbH
$InstallDir = "$env:ProgramData\IT Nexus Agent"
$AgentKey = "cereda-agent-2024-secure-key"
$ServerUrl = "https://it-nexus.cereda-systems.de"
$TaskName = "IT Nexus Agent"
Write-Host "=== IT Nexus Agent Installation ===" -ForegroundColor Cyan
# Verzeichnis erstellen
if (-not (Test-Path $InstallDir)) {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
# Dateien kopieren
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Copy-Item "$ScriptDir\it-nexus-agent.ps1" "$InstallDir\it-nexus-agent.ps1" -Force
# Config schreiben
$config = @{ server_url = $ServerUrl; agent_key = $AgentKey } | ConvertTo-Json
Set-Content -Path "$InstallDir\config.json" -Value $config -Force
# Alten Task entfernen falls vorhanden
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
# Scheduled Task erstellen (laeuft alle 5 Minuten als SYSTEM)
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File `"$InstallDir\it-nexus-agent.ps1`""
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 5) -Once -At (Get-Date)
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -MultipleInstances IgnoreNew -StartWhenAvailable
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest -ErrorAction SilentlyContinue
if (-not $principal) {
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -RunLevel Highest
}
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null
Write-Host "Installiert in: $InstallDir" -ForegroundColor Green
Write-Host "Scheduled Task '$TaskName' erstellt (alle 5 Min)" -ForegroundColor Green
Write-Host "Server: $ServerUrl" -ForegroundColor Green
Write-Host ""
Write-Host "Erster Checkin wird jetzt ausgefuehrt..." -ForegroundColor Yellow
Start-ScheduledTask -TaskName $TaskName
Write-Host "Fertig!" -ForegroundColor Green

View File

@@ -1,606 +0,0 @@
# IT Nexus Monitoring Agent v1.3
# Cereda Systems GmbH
# Laedt Systemdaten und sendet sie an IT Nexus
$AgentVersion = "1.3.1"
$ConfigPath = Join-Path $PSScriptRoot "config.json"
$LogPath = Join-Path $PSScriptRoot "agent.log"
function Write-Log($msg) {
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg"
Add-Content -Path $LogPath -Value $line -ErrorAction SilentlyContinue
}
# Config laden
if (-not (Test-Path $ConfigPath)) {
Write-Log "ERROR: config.json nicht gefunden"
exit 1
}
$config = Get-Content $ConfigPath | ConvertFrom-Json
$ServerUrl = $config.server_url.TrimEnd('/')
$AgentKey = $config.agent_key
function Get-CpuUsage {
try {
$pc = [System.Diagnostics.PerformanceCounter]::new("Processor", "% Processor Time", "_Total")
$pc.NextValue() | Out-Null
Start-Sleep -Milliseconds 800
return [math]::Round($pc.NextValue(), 1)
} catch { return $null }
}
function Get-RamInfo {
try {
$os = Get-CimInstance Win32_OperatingSystem
$total = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
$free = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
return @{ total = $total; used = [math]::Round($total - $free, 2) }
} catch { return @{ total = $null; used = $null } }
}
function Get-DiskInfo {
try {
$disk = Get-PSDrive C
$total = [math]::Round(($disk.Used + $disk.Free) / 1GB, 2)
$free = [math]::Round($disk.Free / 1GB, 2)
return @{ total = $total; free = $free }
} catch { return @{ total = $null; free = $null } }
}
function Get-CpuInfo {
try {
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
return @{ model = $cpu.Name.Trim(); cores = $cpu.NumberOfCores }
} catch { return @{ model = $null; cores = $null } }
}
function Get-MacAddress {
try {
$mac = (Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled } | Select-Object -First 1).MACAddress
return $mac
} catch { return $null }
}
function Get-InstalledSoftware {
try {
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$apps = Get-ItemProperty $paths -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and $_.DisplayName -notmatch '^KB\d+' } |
Select-Object -ExpandProperty DisplayName -Unique |
ForEach-Object { [System.Text.RegularExpressions.Regex]::Replace($_, '[^\x20-\x7E]', '') } |
Where-Object { $_.Length -gt 2 } |
Sort-Object |
Select-Object -First 100
return $apps
} catch { return @() }
}
function Get-PendingUpdates {
try {
$session = New-Object -ComObject Microsoft.Update.Session
$searcher = $session.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0 and Type='Software'")
return $result.Updates.Count
} catch { return 0 }
}
function Get-UptimeHours {
try {
$uptime = (Get-Date) - (gcim Win32_OperatingSystem).LastBootUpTime
return [math]::Round($uptime.TotalHours, 1)
} catch { return $null }
}
function Get-LastUser {
try {
$user = (Get-CimInstance Win32_ComputerSystem).UserName
if ($user) { return $user.Split('\')[-1] }
$profile = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*' |
Where-Object { $_.ProfileImagePath -like 'C:\Users\*' -and $_.ProfileImagePath -notlike '*default*' } |
Sort-Object { $_.PSChildName } | Select-Object -Last 1
return ($profile.ProfileImagePath -split '\\')[-1]
} catch { return $null }
}
function Get-TpmInfo {
try {
$tpm = Get-WmiObject -Namespace "Root\CIMv2\Security\MicrosoftTpm" -Class Win32_Tpm -ErrorAction Stop
if ($tpm) {
$specVer = $tpm.SpecVersion
$isV2 = $specVer -like "*2.0*"
return @{ present = $true; version = $specVer; is_v2 = $isV2 }
}
return @{ present = $false; version = $null; is_v2 = $false }
} catch {
return @{ present = $false; version = $null; is_v2 = $false }
}
}
function Get-SecureBootStatus {
try {
$sb = Confirm-SecureBootUEFI -ErrorAction Stop
return [bool]$sb
} catch {
return $false
}
}
function Get-Win11Readiness {
$tpm = Get-TpmInfo
$sb = Get-SecureBootStatus
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$ram = (Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize / 1MB
$cpuOk = $cpu -and $cpu.NumberOfCores -ge 2 -and $cpu.MaxClockSpeed -ge 1000
$ramOk = $ram -ge 4
$ready = $tpm.is_v2 -and $sb -and $cpuOk -and $ramOk
return @{
tpm_present = $tpm.present
tpm_version = if ($tpm.version) { $tpm.version } else { $null }
tpm_v2 = $tpm.is_v2
secure_boot = $sb
win11_ready = $ready
}
}
# Systemdaten sammeln
Write-Log "Sammle Systemdaten..."
$cpu = Get-CpuInfo
$ram = Get-RamInfo
$disk = Get-DiskInfo
$cpuPct = Get-CpuUsage
$mac = Get-MacAddress
$updates = Get-PendingUpdates
$uptime = Get-UptimeHours
$lastUser = Get-LastUser
$software = Get-InstalledSoftware
$win11 = Get-Win11Readiness
$ip = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.InterfaceAlias -notlike '*Loopback*' -and
$_.InterfaceAlias -notlike '*vEthernet*' -and
$_.InterfaceAlias -notlike '*WSL*' -and
$_.InterfaceAlias -notlike '*Virtual*' -and
$_.IPAddress -notlike '169.*' -and
$_.IPAddress -notlike '172.*'
} | Select-Object -First 1).IPAddress
if (-not $ip) {
$ip = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.InterfaceAlias -notlike '*Loopback*' -and
$_.InterfaceAlias -notlike '*vEthernet*' -and
$_.IPAddress -notlike '169.*'
} | Select-Object -First 1).IPAddress
}
$os = Get-CimInstance Win32_OperatingSystem
$domain = $env:USERDOMAIN
$payload = @{
hostname = $env:COMPUTERNAME
ip_address = $ip
mac_address = $mac
os_name = $os.Caption
os_version = $os.Version
cpu_model = $cpu.model
cpu_cores = $cpu.cores
cpu_usage_percent = $cpuPct
ram_total_gb = $ram.total
ram_used_gb = $ram.used
disk_total_gb = $disk.total
disk_free_gb = $disk.free
last_user = $lastUser
uptime_hours = $uptime
domain = $domain
agent_version = $AgentVersion
installed_software = $software
windows_updates_pending = $updates
tpm_present = $win11.tpm_present
tpm_version = $win11.tpm_version
tpm_v2 = $win11.tpm_v2
secure_boot = $win11.secure_boot
win11_ready = $win11.win11_ready
} | ConvertTo-Json -Depth 3
# --- An IT Nexus senden ---
function Invoke-PatchCommand($cmd, $cmdId) {
Write-Log "PATCH: Command empfangen: $($cmd) (ID: $cmdId)"
$result = "OK"
try {
switch ($cmd) {
'check_updates' {
UsoClient.exe StartScan 2>$null
Start-Sleep -Seconds 5
$result = "Update-Scan gestartet"
Write-Log "PATCH: Update-Scan gestartet"
}
'install_updates' {
UsoClient.exe StartDownload 2>$null
Start-Sleep -Seconds 3
UsoClient.exe StartInstall 2>$null
$result = "Update-Installation gestartet"
Write-Log "PATCH: Update-Installation gestartet"
}
'update_agent' {
Write-Log "UPDATE: Manuelles Agent-Update angefordert..."
try {
$scriptPath = $MyInvocation.MyCommand.Path
$tempPath = "$scriptPath.update"
$updateHeaders = @{ 'X-Agent-Key' = $AgentKey }
Invoke-WebRequest -Uri "$ServerUrl/api/monitoring/agent-script" `
-Headers $updateHeaders -OutFile $tempPath -TimeoutSec 30
if ((Get-Item $tempPath -ErrorAction SilentlyContinue).Length -gt 1024) {
Copy-Item -Path $tempPath -Destination $scriptPath -Force
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
$result = "Agent erfolgreich aktualisiert - neue Version aktiv ab naechstem Run"
Write-Log "UPDATE: Manuelles Update erfolgreich"
} else {
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
$result = "Fehler: Heruntergeladene Datei ungueltig"
Write-Log "UPDATE ERROR: Datei zu klein"
}
} catch {
$result = "Fehler: $($_.Exception.Message)"
Write-Log "UPDATE ERROR: $result"
}
}
'upgrade_win11' {
Write-Log "WIN11: Starte Windows 11 Upgrade-Prozess..."
$assistantPath = "C:\ProgramData\IT Nexus Agent\Win11Upgrade.exe"
try {
Write-Log "WIN11: Lade Installation Assistant herunter..."
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2171764" -OutFile $assistantPath -TimeoutSec 300
if ((Get-Item $assistantPath -ErrorAction SilentlyContinue).Length -gt 1MB) {
# Als angemeldeter Benutzer ausfuehren damit UI sichtbar ist
$loggedInUser = (Get-CimInstance Win32_ComputerSystem).UserName
if ($loggedInUser -and $loggedInUser -ne '') {
Unregister-ScheduledTask -TaskName "IT Nexus Win11 Upgrade" -Confirm:$false -ErrorAction SilentlyContinue
$action = New-ScheduledTaskAction -Execute $assistantPath -Argument "/skipeula /auto upgrade"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(20)
$principal = New-ScheduledTaskPrincipal -UserId $loggedInUser -LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 3)
Register-ScheduledTask -TaskName "IT Nexus Win11 Upgrade" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
Start-ScheduledTask -TaskName "IT Nexus Win11 Upgrade"
$reportBody = @{ command_id = $cmdId; status = 'running'; result = "Windows 11 Upgrade gestartet fuer Benutzer $loggedInUser - Fortschritt sichtbar auf dem Geraet" } | ConvertTo-Json
Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" -Method POST -Body $reportBody -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } -TimeoutSec 10 -ErrorAction SilentlyContinue
Write-Log "WIN11: Upgrade als $loggedInUser gestartet (Status: running)"
return
} else {
Start-Process -FilePath $assistantPath -ArgumentList "/quietinstall /skipeula /auto upgrade" -NoNewWindow
$reportBody = @{ command_id = $cmdId; status = 'running'; result = "Windows 11 Upgrade gestartet (kein Benutzer angemeldet)" } | ConvertTo-Json
Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" -Method POST -Body $reportBody -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } -TimeoutSec 10 -ErrorAction SilentlyContinue
Write-Log "WIN11: Upgrade als SYSTEM gestartet (kein User)"
return
}
} else {
$result = "Fehler: Download fehlgeschlagen oder Datei zu klein"
Write-Log "WIN11 ERROR: Download fehlgeschlagen"
}
} catch {
$result = "Fehler: $($_.Exception.Message)"
Write-Log "WIN11 ERROR: $result"
}
}
'reboot' {
$result = "Neustart wird in 60 Sekunden durchgefuehrt"
Write-Log "PATCH: Neustart geplant"
$reportBody = @{ command_id = $cmdId; status = 'done'; result = $result } | ConvertTo-Json
Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" `
-Method POST -Body $reportBody `
-Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } `
-TimeoutSec 10 -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
shutdown.exe /r /t 60 /c "IT Nexus Patch Management - Geplanter Neustart"
return
}
}
} catch {
$result = "Fehler: $($_.Exception.Message)"
Write-Log "PATCH ERROR: $result"
}
try {
$reportBody = @{ command_id = $cmdId; status = 'done'; result = $result } | ConvertTo-Json
Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" `
-Method POST -Body $reportBody `
-Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } `
-TimeoutSec 10
} catch {
Write-Log "PATCH: Ergebnis-Meldung fehlgeschlagen: $($_.Exception.Message)"
}
}
try {
$headers = @{
'Content-Type' = 'application/json'
'X-Agent-Key' = $AgentKey
}
$cleanPayload = $payload -replace '[\x00-\x1F\x7F]', ''
$response = Invoke-RestMethod -Uri "$ServerUrl/api/monitoring/checkin" `
-Method POST -Body $cleanPayload -Headers $headers -TimeoutSec 30
Write-Log "OK: Checkin erfolgreich (Hostname: $env:COMPUTERNAME)"
# Self-Update pruefen
$serverVersion = $response.agent_version
if ($serverVersion -and $serverVersion -ne $AgentVersion) {
Write-Log "UPDATE: Neue Agent-Version verfuegbar: $serverVersion (aktuell: $AgentVersion)"
try {
$scriptPath = $MyInvocation.MyCommand.Path
$tempPath = "$scriptPath.update"
$updateHeaders = @{ 'X-Agent-Key' = $AgentKey }
Invoke-WebRequest -Uri "$ServerUrl/api/monitoring/agent-script" `
-Headers $updateHeaders -OutFile $tempPath -TimeoutSec 30
if ((Get-Item $tempPath).Length -gt 1024) {
Copy-Item -Path $tempPath -Destination $scriptPath -Force
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
Write-Log "UPDATE: Agent erfolgreich auf Version $serverVersion aktualisiert."
} else {
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
Write-Log "UPDATE: Heruntergeladene Datei zu klein - Update abgebrochen"
}
} catch {
Write-Log "UPDATE ERROR: $($_.Exception.Message)"
}
}
# Laufende Commands pruefen (z.B. Win11 Upgrade)
if ($response.running_commands -and $response.running_commands.Count -gt 0) {
foreach ($rc in $response.running_commands) {
if ($rc.command -eq 'upgrade_win11') {
$task = Get-ScheduledTask -TaskName "IT Nexus Win11 Upgrade" -ErrorAction SilentlyContinue
$proc = Get-Process -Name "Win11Upgrade" -ErrorAction SilentlyContinue
$stillRunning = ($task -and $task.State -eq 'Running') -or ($proc -ne $null)
if (-not $stillRunning) {
$taskInfo = Get-ScheduledTaskInfo -TaskName "IT Nexus Win11 Upgrade" -ErrorAction SilentlyContinue
$exitCode = if ($taskInfo) { $taskInfo.LastTaskResult } else { 0 }
$doneResult = if ($exitCode -eq 0) { "Windows 11 Upgrade abgeschlossen" } else { "Upgrade beendet (Code: $exitCode)" }
$rb = @{ command_id = $rc.id; status = 'done'; result = $doneResult } | ConvertTo-Json
Invoke-RestMethod -Uri "$ServerUrl/api/patch/commands/result" -Method POST -Body $rb -Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } -TimeoutSec 10 -ErrorAction SilentlyContinue
Write-Log "WIN11: Upgrade abgeschlossen (Code: $exitCode)"
} else {
Write-Log "WIN11: Upgrade laeuft noch..."
}
}
}
}
# Ankuendigungen anzeigen (als eingeloggter Benutzer via ScheduledTask)
if ($response.announcements -and $response.announcements.Count -gt 0) {
foreach ($ann in $response.announcements) {
try {
$annId = $ann.id
$annTitle = $ann.title -replace "'", "''"
$annMessage = $ann.message -replace "'", "''"
$typeLabel = switch ($ann.type) {
'warning' { 'WICHTIGE WARNUNG' }
'maintenance' { 'WARTUNGSANKUENDIGUNG' }
default { 'INFORMATION' }
}
$accentColor = switch ($ann.type) {
'warning' { '220, 50, 50' }
'maintenance' { '245, 158, 11' }
default { '99, 102, 241' }
}
# PS-Script das im Benutzer-Kontext laeuft und den Dialog zeigt
$dialogScript = @"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
`$form = New-Object System.Windows.Forms.Form
`$form.Text = 'IT Nexus - $typeLabel'
`$form.Size = New-Object System.Drawing.Size(520, 330)
`$form.StartPosition = 'CenterScreen'
`$form.FormBorderStyle = 'FixedDialog'
`$form.MaximizeBox = `$false
`$form.MinimizeBox = `$false
`$form.TopMost = `$true
`$form.BackColor = [System.Drawing.Color]::FromArgb(24, 24, 37)
`$lTitle = New-Object System.Windows.Forms.Label
`$lTitle.Text = '$annTitle'
`$lTitle.Font = New-Object System.Drawing.Font('Segoe UI', 13, [System.Drawing.FontStyle]::Bold)
`$lTitle.ForeColor = [System.Drawing.Color]::White
`$lTitle.Location = New-Object System.Drawing.Point(20, 20)
`$lTitle.Size = New-Object System.Drawing.Size(460, 36)
`$form.Controls.Add(`$lTitle)
`$lMsg = New-Object System.Windows.Forms.Label
`$lMsg.Text = '$annMessage'
`$lMsg.Font = New-Object System.Drawing.Font('Segoe UI', 10)
`$lMsg.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 220)
`$lMsg.Location = New-Object System.Drawing.Point(20, 66)
`$lMsg.Size = New-Object System.Drawing.Size(460, 155)
`$lMsg.AutoSize = `$false
`$form.Controls.Add(`$lMsg)
`$lHint = New-Object System.Windows.Forms.Label
`$lHint.Text = 'Bitte lesen und Kenntnisnahme bestaetigen.'
`$lHint.Font = New-Object System.Drawing.Font('Segoe UI', 8, [System.Drawing.FontStyle]::Italic)
`$lHint.ForeColor = [System.Drawing.Color]::FromArgb(130, 130, 150)
`$lHint.Location = New-Object System.Drawing.Point(20, 228)
`$lHint.Size = New-Object System.Drawing.Size(460, 20)
`$form.Controls.Add(`$lHint)
`$btn = New-Object System.Windows.Forms.Button
`$btn.Text = 'Gelesen und bestaetigt'
`$btn.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)
`$btn.ForeColor = [System.Drawing.Color]::White
`$btn.BackColor = [System.Drawing.Color]::FromArgb($accentColor)
`$btn.FlatStyle = 'Flat'
`$btn.Location = New-Object System.Drawing.Point(20, 255)
`$btn.Size = New-Object System.Drawing.Size(460, 40)
`$btn.DialogResult = [System.Windows.Forms.DialogResult]::OK
`$form.Controls.Add(`$btn)
`$form.AcceptButton = `$btn
`$r = `$form.ShowDialog()
if (`$r -eq [System.Windows.Forms.DialogResult]::OK) {
`$b = [System.Text.Encoding]::UTF8.GetBytes('{"hostname":"' + `$env:COMPUTERNAME + '"}')
`$req = [System.Net.WebRequest]::Create('$ServerUrl/api/announcements/$annId/ack-agent')
`$req.Method = 'POST'
`$req.ContentType = 'application/json'
`$req.Headers.Add('X-Agent-Key','$AgentKey')
`$req.ContentLength = `$b.Length
`$s = `$req.GetRequestStream(); `$s.Write(`$b,0,`$b.Length); `$s.Close()
try { `$req.GetResponse().Close() } catch {}
}
"@
# Dialog-Script temporaer speichern
$scriptFile = "C:\ProgramData\IT Nexus Agent\ann_$annId.ps1"
$dialogScript | Out-File -FilePath $scriptFile -Encoding UTF8 -Force
# Eingeloggten Benutzer ermitteln
$loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName
if ($loggedUser -and $loggedUser -ne '') {
$taskName = "ITNexus-Ann-$annId"
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Normal -File `"$scriptFile`""
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3)
$principal = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -StartWhenAvailable
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
Write-Log "ANNOUNCEMENT: Dialog fuer Benutzer $loggedUser gestartet - ID $annId"
} else {
Write-Log "ANNOUNCEMENT: Kein Benutzer eingeloggt - Dialog wird beim naechsten Check-in versucht"
}
} catch {
Write-Log "ANNOUNCEMENT ERROR: $($_.Exception.Message)"
}
}
}
# Pending Patch-Commands verarbeiten
if ($response.commands -and $response.commands.Count -gt 0) {
Write-Log "PATCH: $($response.commands.Count) Command(s) empfangen"
foreach ($cmd in $response.commands) {
Invoke-PatchCommand -cmd $cmd.command -cmdId $cmd.id
}
}
} catch {
$statusCode = $_.Exception.Response.StatusCode.Value__
Write-Log "ERROR: Checkin fehlgeschlagen - HTTP $statusCode - $($_.Exception.Message)"
try {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
Write-Log "ERROR Detail: $($reader.ReadToEnd())"
} catch {}
}
# ── Schneller Announcement-Poll (alle 15 Sekunden im Hintergrund) ─────────────
function Invoke-AnnouncementPoll {
try {
$pollBody = @{ hostname = $env:COMPUTERNAME } | ConvertTo-Json
$pollResp = Invoke-RestMethod -Uri "$ServerUrl/api/monitoring/announcements-poll" `
-Method POST -Body $pollBody `
-Headers @{ 'Content-Type' = 'application/json'; 'X-Agent-Key' = $AgentKey } `
-TimeoutSec 10
if ($pollResp.announcements -and $pollResp.announcements.Count -gt 0) {
foreach ($ann in $pollResp.announcements) {
$annId = $ann.id
$annTitle = $ann.title -replace "'", "''"
$annMessage = $ann.message -replace "'", "''"
$accentColor = switch ($ann.type) {
'warning' { '220, 50, 50' }
'maintenance' { '245, 158, 11' }
default { '99, 102, 241' }
}
$typeLabel = switch ($ann.type) {
'warning' { 'WICHTIGE WARNUNG' }
'maintenance' { 'WARTUNGSANKUENDIGUNG' }
default { 'INFORMATION' }
}
$dialogScript = @"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
`$form = New-Object System.Windows.Forms.Form
`$form.Text = 'IT Nexus - $typeLabel'
`$form.Size = New-Object System.Drawing.Size(520, 330)
`$form.StartPosition = 'CenterScreen'
`$form.FormBorderStyle = 'FixedDialog'
`$form.MaximizeBox = `$false
`$form.MinimizeBox = `$false
`$form.TopMost = `$true
`$form.BackColor = [System.Drawing.Color]::FromArgb(24, 24, 37)
`$lTitle = New-Object System.Windows.Forms.Label
`$lTitle.Text = '$annTitle'
`$lTitle.Font = New-Object System.Drawing.Font('Segoe UI', 13, [System.Drawing.FontStyle]::Bold)
`$lTitle.ForeColor = [System.Drawing.Color]::White
`$lTitle.Location = New-Object System.Drawing.Point(20, 20)
`$lTitle.Size = New-Object System.Drawing.Size(460, 36)
`$form.Controls.Add(`$lTitle)
`$lMsg = New-Object System.Windows.Forms.Label
`$lMsg.Text = '$annMessage'
`$lMsg.Font = New-Object System.Drawing.Font('Segoe UI', 10)
`$lMsg.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 220)
`$lMsg.Location = New-Object System.Drawing.Point(20, 66)
`$lMsg.Size = New-Object System.Drawing.Size(460, 155)
`$lMsg.AutoSize = `$false
`$form.Controls.Add(`$lMsg)
`$lHint = New-Object System.Windows.Forms.Label
`$lHint.Text = 'Bitte lesen und Kenntnisnahme bestaetigen.'
`$lHint.Font = New-Object System.Drawing.Font('Segoe UI', 8, [System.Drawing.FontStyle]::Italic)
`$lHint.ForeColor = [System.Drawing.Color]::FromArgb(130, 130, 150)
`$lHint.Location = New-Object System.Drawing.Point(20, 228)
`$lHint.Size = New-Object System.Drawing.Size(460, 20)
`$form.Controls.Add(`$lHint)
`$btn = New-Object System.Windows.Forms.Button
`$btn.Text = 'Gelesen und bestaetigt'
`$btn.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)
`$btn.ForeColor = [System.Drawing.Color]::White
`$btn.BackColor = [System.Drawing.Color]::FromArgb($accentColor)
`$btn.FlatStyle = 'Flat'
`$btn.Location = New-Object System.Drawing.Point(20, 255)
`$btn.Size = New-Object System.Drawing.Size(460, 40)
`$btn.DialogResult = [System.Windows.Forms.DialogResult]::OK
`$form.Controls.Add(`$btn)
`$form.AcceptButton = `$btn
`$r = `$form.ShowDialog()
if (`$r -eq [System.Windows.Forms.DialogResult]::OK) {
`$b = [System.Text.Encoding]::UTF8.GetBytes('{"hostname":"' + `$env:COMPUTERNAME + '"}')
`$req = [System.Net.WebRequest]::Create('$ServerUrl/api/announcements/$annId/ack-agent')
`$req.Method = 'POST'; `$req.ContentType = 'application/json'
`$req.Headers.Add('X-Agent-Key','$AgentKey')
`$req.ContentLength = `$b.Length
`$s = `$req.GetRequestStream(); `$s.Write(`$b,0,`$b.Length); `$s.Close()
try { `$req.GetResponse().Close() } catch {}
}
"@
$scriptFile = "C:\ProgramData\IT Nexus Agent\ann_$annId.ps1"
$dialogScript | Out-File -FilePath $scriptFile -Encoding UTF8 -Force
$loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName
if ($loggedUser) {
$taskName = "ITNexus-Ann-$annId"
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Normal -File `"$scriptFile`""
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(2)
$principal = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -StartWhenAvailable
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
Write-Log "ANNOUNCEMENT: Sofort-Dialog gestartet fuer $loggedUser - ID $annId"
}
}
}
} catch {
# Stiller Fehler - nicht kritisch
}
}
# Announcement-Watcher-Task einrichten (alle 15 Sek, bleibt dauerhaft aktiv)
try {
$watcherName = 'IT Nexus Announcement Watcher'
$watcherScript = 'C:\ProgramData\IT Nexus Agent\ann-watcher.ps1'
# Watcher-Script vom Server laden (immer aktuell)
Invoke-WebRequest -Uri "$ServerUrl/api/monitoring/ann-watcher" `
-Headers @{ 'X-Agent-Key' = $AgentKey } `
-OutFile $watcherScript -TimeoutSec 15 -ErrorAction SilentlyContinue
# Task registrieren falls noch nicht vorhanden
if (-not (Get-ScheduledTask -TaskName $watcherName -ErrorAction SilentlyContinue)) {
$wAct = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File `"$watcherScript`""
$wTrg = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Seconds 15) -Once -At (Get-Date)
$wSet = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 14) -MultipleInstances IgnoreNew -StartWhenAvailable
$wPri = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $watcherName -Action $wAct -Trigger $wTrg -Settings $wSet -Principal $wPri -Force | Out-Null
Write-Log "ANNOUNCEMENT: Watcher-Task registriert (alle 15 Sekunden)"
}
} catch {
Write-Log "ANNOUNCEMENT WATCHER ERROR: $($_.Exception.Message)"
}

View File

@@ -1,103 +0,0 @@
#define MyAppName "IT Nexus Agent"
#define MyAppVersion "1.2.4"
#define MyAppPublisher "Cereda Systems GmbH"
#define MyAppURL "https://it-nexus.cereda-systems.de"
#define MyAppExeName "it-nexus-agent.ps1"
#define InstallDir "{commonappdata}\IT Nexus Agent"
[Setup]
AppId={{B3F7A2C1-4E8D-4F2A-9B1C-7D3E5F6A8B2C}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
DefaultDirName={#InstallDir}
DisableDirPage=yes
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
OutputDir=..\dist
OutputBaseFilename=IT-Nexus-Agent-Setup-v{#MyAppVersion}
SetupIconFile=
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
WizardSizePercent=120
PrivilegesRequired=admin
UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={app}\it-nexus-agent.ps1
CloseApplications=no
DisableWelcomePage=no
WizardImageFile=compiler:WizModernImage.bmp
WizardSmallImageFile=compiler:WizModernSmallImage.bmp
[Languages]
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Files]
Source: "it-nexus-agent.ps1"; DestDir: "{app}"; Flags: ignoreversion
Source: "config.json.template"; DestDir: "{app}"; DestName: "config.json"; Flags: ignoreversion onlyifdoesntexist
[Run]
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -NonInteractive -Command ""Unregister-ScheduledTask -TaskName 'IT Nexus Agent' -Confirm:$false -ErrorAction SilentlyContinue; $a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File \""{app}\it-nexus-agent.ps1\""'; $t = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 1) -Once -At (Get-Date); $s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -MultipleInstances IgnoreNew -StartWhenAvailable; $p = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest; Register-ScheduledTask -TaskName 'IT Nexus Agent' -Action $a -Trigger $t -Settings $s -Principal $p -Force; Start-ScheduledTask -TaskName 'IT Nexus Agent'"""; Flags: runhidden waituntilterminated; StatusMsg: "Registriere Windows Scheduled Task..."
[UninstallRun]
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -NonInteractive -Command ""Unregister-ScheduledTask -TaskName 'IT Nexus Agent' -Confirm:$false -ErrorAction SilentlyContinue"""; Flags: runhidden waituntilterminated
[Code]
var
ServerUrlPage: TInputQueryWizardPage;
AgentKeyPage: TInputQueryWizardPage;
procedure InitializeWizard;
begin
ServerUrlPage := CreateInputQueryPage(wpWelcome,
'Server-Konfiguration',
'Gib die IT Nexus Server-Adresse ein.',
'');
ServerUrlPage.Add('Server URL:', False);
ServerUrlPage.Values[0] := 'https://it-nexus.cereda-systems.de';
AgentKeyPage := CreateInputQueryPage(ServerUrlPage.ID,
'API Key',
'Gib den Agent API Key ein. Diesen findest du in den IT Nexus Einstellungen.',
'');
AgentKeyPage.Add('Agent API Key:', False);
AgentKeyPage.Values[0] := 'itx-4CPJPTHmCfdrL9D62WacCATEvuvULXcp7ECMpSaNUjsS344F6_L4Ug';
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
Result := True;
if CurPageID = ServerUrlPage.ID then begin
if ServerUrlPage.Values[0] = '' then begin
MsgBox('Bitte gib eine Server URL ein.', mbError, MB_OK);
Result := False;
end;
end;
if CurPageID = AgentKeyPage.ID then begin
if AgentKeyPage.Values[0] = '' then begin
MsgBox('Bitte gib einen API Key ein.', mbError, MB_OK);
Result := False;
end;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
ConfigFile: string;
Config: TStringList;
begin
if CurStep = ssPostInstall then begin
ConfigFile := ExpandConstant('{app}\config.json');
Config := TStringList.Create;
try
Config.Add('{');
Config.Add(' "server_url": "' + ServerUrlPage.Values[0] + '",');
Config.Add(' "agent_key": "' + AgentKeyPage.Values[0] + '"');
Config.Add('}');
Config.SaveToFile(ConfigFile);
finally
Config.Free;
end;
end;
end;

View File

@@ -1,8 +0,0 @@
# IT Nexus Agent - Uninstaller
$InstallDir = "C:\Program Files\IT Nexus Agent"
$TaskName = "IT Nexus Agent"
Write-Host "=== IT Nexus Agent Deinstallation ===" -ForegroundColor Yellow
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
Remove-Item -Path $InstallDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Agent entfernt." -ForegroundColor Green

View File

@@ -14,6 +14,7 @@
"better-sqlite3": "^12.6.2",
"botbuilder": "^4.23.3",
"bwip-js": "^4.8.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
@@ -1371,6 +1372,25 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",

View File

@@ -21,6 +21,7 @@
"better-sqlite3": "^12.6.2",
"botbuilder": "^4.23.3",
"bwip-js": "^4.8.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",

View File

@@ -1,14 +1,14 @@
require('dotenv').config();
if (!process.env.JWT_SECRET) {
console.error('❌ FATAL: JWT_SECRET ist nicht gesetzt. Server wird nicht mit einem unsicheren Default-Secret gestartet.');
process.exit(1);
}
const JWT_CONFIG = {
secret: process.env.JWT_SECRET || 'default-secret-change-in-production',
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRATION || '8h',
algorithm: 'HS256'
};
// Validate that JWT_SECRET is set
if (!process.env.JWT_SECRET) {
console.warn('⚠️ WARNING: JWT_SECRET not set in .env file. Using default secret (INSECURE!)');
}
module.exports = JWT_CONFIG;

View File

@@ -160,8 +160,12 @@ class AiController {
const { url } = req.body;
if (!url?.trim()) throw new AppError('URL ist erforderlich', 400);
// Only allow http/https
if (!/^https?:\/\//i.test(url)) throw new AppError('Nur HTTP/HTTPS URLs erlaubt', 400);
const { assertPublicUrl } = require('../utils/ssrfGuard');
try {
await assertPublicUrl(url);
} catch (e) {
throw new AppError(e.message, 400);
}
let html;
try {
@@ -204,7 +208,12 @@ class AiController {
const AiService = require('../services/ai.service');
const { url, maxPages = 20 } = req.body;
if (!url?.trim()) throw new AppError('URL ist erforderlich', 400);
if (!/^https?:\/\//i.test(url)) throw new AppError('Nur HTTP/HTTPS URLs erlaubt', 400);
const { assertPublicUrl } = require('../utils/ssrfGuard');
try {
await assertPublicUrl(url);
} catch (e) {
throw new AppError(e.message, 400);
}
const limit = Math.min(Math.max(1, parseInt(maxPages) || 20), 100);
const baseUrl = new URL(url);
@@ -236,6 +245,7 @@ class AiController {
visited.add(currentUrl);
try {
try { await assertPublicUrl(currentUrl); } catch { continue; } // DNS-Rebinding-Schutz
const response = await fetch(currentUrl, {
headers: { 'User-Agent': 'Mozilla/5.0 IT-Nexus KnowledgeBase Importer' },
signal: AbortSignal.timeout(10000),

View File

@@ -1,6 +1,7 @@
const AuthService = require('../services/auth.service');
const User = require('../models/User');
const { asyncHandler } = require('../middleware/errorHandler');
const { setAuthCookie, clearAuthCookie } = require('../utils/authCookie');
class AuthController {
/**
@@ -18,10 +19,11 @@ class AuthController {
}
const result = await AuthService.login(username, password);
setAuthCookie(res, result.token);
res.json({
status: 'success',
data: result
data: { user: result.user }
});
});
@@ -104,8 +106,7 @@ class AuthController {
* POST /api/auth/logout
*/
static logout = asyncHandler(async (req, res) => {
// Client-side will handle token removal
// This endpoint is just for consistency and potential future server-side session handling
clearAuthCookie(res);
res.json({
status: 'success',
message: 'Logged out successfully'

View File

@@ -4,6 +4,60 @@ const { asyncHandler } = require('../middleware/errorHandler');
const { getPendingCommands } = require('./patch.controller');
const { getDatabase } = require('../config/database');
const { getForAgent } = require('./announcement.controller');
const { validateAgentKey } = require('../utils/agentAuth');
const crypto = require('crypto');
// Code-seitiger Fallback falls AGENT_VERSION env-Var fehlt — bei jedem Agent-Release mit hochziehen!
// (War zuvor an mehreren Stellen hartcodiert auf alte Versionen wie '2.0.0'/'1.2.4' — echter
// Downgrade-Bug für Agents ohne Patch-Gruppe oder ohne ?hostname-Parameter.)
const FALLBACK_AGENT_VERSION = '2.8.0';
// Vergleicht zwei "x.y.z"-Versionsstrings. >0 wenn a>b, <0 wenn a<b, 0 wenn gleich.
function compareVersions(a, b) {
const pa = String(a || '0').split('.').map(n => parseInt(n, 10) || 0);
const pb = String(b || '0').split('.').map(n => parseInt(n, 10) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const diff = (pa[i] || 0) - (pb[i] || 0);
if (diff !== 0) return diff;
}
return 0;
}
// Nie eine ältere Version als die aktuell installierte vorschlagen/ausliefern — verhindert
// Downgrades durch fehlerhafte ENV-Konfiguration, falsch gesetzte Gruppen-Zielversionen o.ä.
function clampToNotOlder(targetVersion, currentVersion) {
if (!currentVersion) return targetVersion;
return compareVersions(targetVersion, currentVersion) < 0 ? currentVersion : targetVersion;
}
// POST /api/monitoring/enroll — Agent tauscht geteilten Bootstrap-Key gegen individuellen Per-Device-Key.
// Idempotent: ein bereits enrollter Agent bekommt seinen bestehenden Key einfach erneut zurück.
const enroll = asyncHandler(async (req, res) => {
const agentKey = req.headers['x-agent-key'];
const { hostname } = req.body;
if (!hostname) return res.status(400).json({ status: 'error', message: 'hostname required' });
const db = getDatabase();
const existing = db.prepare('SELECT id, agent_key FROM monitoring_agents WHERE hostname = ?').get(hostname);
const validBootstrap = agentKey === process.env.AGENT_API_KEY;
const validExisting = existing?.agent_key && agentKey === existing.agent_key;
if (!validBootstrap && !validExisting) {
return res.status(401).json({ status: 'error', message: 'Invalid agent key' });
}
let key = existing?.agent_key;
if (!key) {
key = 'itx-' + crypto.randomBytes(24).toString('hex');
if (existing) {
db.prepare('UPDATE monitoring_agents SET agent_key = ? WHERE id = ?').run(key, existing.id);
} else {
db.prepare('INSERT INTO monitoring_agents (hostname, agent_key) VALUES (?, ?)').run(hostname, key);
}
console.log(`[Enroll] Neuer Per-Device-Key ausgestellt für ${hostname}`);
}
res.json({ status: 'success', data: { agent_key: key } });
});
function extractManufacturer(modelStr) {
if (!modelStr) return null;
@@ -75,14 +129,13 @@ function syncAgentToAsset(data) {
// POST /api/monitoring/checkin — called by agent (no JWT, uses API key)
const checkin = asyncHandler(async (req, res) => {
const agentKey = req.headers['x-agent-key'];
if (!agentKey || agentKey !== process.env.AGENT_API_KEY) {
return res.status(401).json({ status: 'error', message: 'Invalid agent key' });
}
const { hostname } = req.body;
if (!hostname) {
return res.status(400).json({ status: 'error', message: 'hostname required' });
}
if (!validateAgentKey(agentKey, hostname)) {
return res.status(401).json({ status: 'error', message: 'Invalid agent key' });
}
const agent = MonitoringAgent.upsert(req.body);
@@ -93,7 +146,7 @@ const checkin = asyncHandler(async (req, res) => {
const { pending, running } = getPendingCommands(agent.id);
// Zielversion: Gruppen-spezifisch (staged rollout) oder globaler Default
const GLOBAL_VERSION = process.env.AGENT_VERSION || '1.2.4';
const GLOBAL_VERSION = process.env.AGENT_VERSION || FALLBACK_AGENT_VERSION;
let targetVersion = GLOBAL_VERSION;
try {
const db = getDatabase();
@@ -108,6 +161,8 @@ const checkin = asyncHandler(async (req, res) => {
if (groupRow?.target_agent_version) targetVersion = groupRow.target_agent_version;
} catch { /* kein Gruppe zugewiesen → global */ }
targetVersion = clampToNotOlder(targetVersion, req.body.agent_version);
const announcements = getForAgent(agent.id);
res.json({
@@ -133,11 +188,11 @@ const getAll = asyncHandler(async (req, res) => {
// POST /api/monitoring/announcements-poll — leichter Poll nur für Ankündigungen
const announcementsPoll = asyncHandler(async (req, res) => {
const agentKey = req.headers['x-agent-key'];
if (!agentKey || agentKey !== process.env.AGENT_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { hostname } = req.body;
if (!hostname) return res.status(400).json({ error: 'hostname required' });
if (!validateAgentKey(agentKey, hostname)) {
return res.status(401).json({ error: 'Unauthorized' });
}
const agent = MonitoringAgent.getByHostname(hostname);
if (!agent) return res.json({ announcements: [] });
const announcements = getForAgent(agent.id);
@@ -181,7 +236,7 @@ const downloadScript = asyncHandler(async (req, res) => {
return res.status(404).json({ status: 'error', message: 'Script nicht gefunden' });
}
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('X-Agent-Version', process.env.AGENT_VERSION || '1.2.3');
res.setHeader('X-Agent-Version', process.env.AGENT_VERSION || FALLBACK_AGENT_VERSION);
res.sendFile(scriptPath);
});
@@ -204,7 +259,7 @@ const downloadAnnWatcher = asyncHandler(async (req, res) => {
const downloadSetup = asyncHandler(async (req, res) => {
// Agent-Key Auth für auto-update vom Agent selbst
const agentKey = req.headers['x-agent-key'];
const isAgent = agentKey && agentKey === process.env.AGENT_API_KEY;
const isAgent = validateAgentKey(agentKey, req.query.hostname);
const isAdmin = req.user?.role_name === 'admin' || req.user?.role_name === 'super_admin';
if (!isAgent && !isAdmin) {
return res.status(401).json({ status: 'error', message: 'Unauthorized' });
@@ -212,7 +267,27 @@ const downloadSetup = asyncHandler(async (req, res) => {
const path = require('path');
const fs = require('fs');
const version = process.env.AGENT_VERSION || '2.0.0';
// Zielversion: Gruppen-spezifisch (staged rollout) oder globaler Default — gleiche Logik wie checkin()
let version = process.env.AGENT_VERSION || FALLBACK_AGENT_VERSION;
const hostname = req.query.hostname;
if (hostname) {
try {
const db = getDatabase();
const agent = db.prepare('SELECT id, agent_version FROM monitoring_agents WHERE hostname = ?').get(hostname);
const groupRow = agent && db.prepare(`
SELECT pg.target_agent_version
FROM patch_agent_groups pag
JOIN patch_groups pg ON pg.id = pag.group_id
WHERE pag.agent_id = ?
ORDER BY pg.sort_order ASC
LIMIT 1
`).get(agent.id);
if (groupRow?.target_agent_version) version = groupRow.target_agent_version;
version = clampToNotOlder(version, agent?.agent_version);
} catch { /* kein Gruppe zugewiesen → global */ }
}
const setupPath = path.join(__dirname, `../../agent/IT-Nexus-Agent-Setup-v${version}.exe`);
if (!fs.existsSync(setupPath)) {
return res.status(404).json({ status: 'error', message: 'Setup nicht gefunden' });
@@ -223,4 +298,4 @@ const downloadSetup = asyncHandler(async (req, res) => {
res.sendFile(setupPath);
});
module.exports = { checkin, announcementsPoll, getAll, getStatistics, getById, deleteAgent, downloadScript, downloadAnnWatcher, downloadSetup };
module.exports = { checkin, announcementsPoll, getAll, getStatistics, getById, deleteAgent, downloadScript, downloadAnnWatcher, downloadSetup, enroll };

View File

@@ -1,4 +1,72 @@
const { getDatabase } = require('../config/database');
const https = require('https');
// In-memory CVE cache — refresh every hour
let cveCache = { ts: 0, data: [] };
function nvdGet(path) {
return new Promise((resolve, reject) => {
const req = https.get({
hostname: 'services.nvd.nist.gov',
path,
headers: { 'User-Agent': 'IT-Nexus-TV/1.0' },
}, (res) => {
let raw = '';
res.on('data', c => raw += c);
res.on('end', () => { try { resolve(JSON.parse(raw)); } catch (e) { reject(e); } });
});
req.on('error', reject);
setTimeout(() => { req.destroy(); reject(new Error('timeout')); }, 15000);
});
}
function mapCVE(v) {
const cve = v.cve;
const m31 = cve.metrics?.cvssMetricV31 || [];
const m30 = cve.metrics?.cvssMetricV30 || [];
const m2 = cve.metrics?.cvssMetricV2 || [];
const m = m31[0] || m30[0] || m2[0];
const score = m?.cvssData?.baseScore || 0;
let sevRaw = (m?.cvssData?.baseSeverity || m?.baseSeverity || 'medium').toLowerCase();
if (!['critical','high','medium','low'].includes(sevRaw)) {
sevRaw = score >= 9 ? 'critical' : score >= 7 ? 'high' : score >= 4 ? 'medium' : 'low';
}
const desc = cve.descriptions?.find(d => d.lang === 'en')?.value || '';
const sentence = desc.split(/\.\s/)[0].replace(/\s+/g, ' ').trim();
const title = sentence.length > 110 ? sentence.substring(0, 110) + '…' : sentence || cve.id;
const pub = new Date(cve.published);
const diffH = Math.round((Date.now() - pub.getTime()) / 3600000);
const published = diffH < 1 ? '< 1 Std.' : diffH < 24 ? `${diffH} Std.` : `${Math.round(diffH / 24)} Tag${Math.round(diffH / 24) !== 1 ? 'e' : ''}`;
let vendor = 'NVD';
const cpe = cve.configurations?.[0]?.nodes?.[0]?.cpeMatch?.[0]?.criteria || '';
const cm = cpe.match(/cpe:2\.3:[ao]:([^:]+):/);
if (cm) vendor = cm[1].charAt(0).toUpperCase() + cm[1].slice(1).replace(/_/g, ' ');
return { id: cve.id, sev: sevRaw, cvss: score, title, vendor, published };
}
async function fetchCVEs() {
try {
// Step 1: get total count (fast, no filters)
const r1 = await nvdGet('/rest/json/cves/2.0?resultsPerPage=1');
const total = r1.totalResults || 0;
if (total < 5) return null;
// Step 2: fetch the last ~60 entries (newest published CVEs)
const startIndex = Math.max(0, total - 60);
const r2 = await nvdGet(`/rest/json/cves/2.0?resultsPerPage=60&startIndex=${startIndex}`);
const vulns = (r2.vulnerabilities || []).reverse(); // newest first
const mapped = vulns.map(mapCVE).filter(c => c.cvss >= 5);
// Prefer high/critical, then fill with medium
const high = mapped.filter(c => ['critical','high'].includes(c.sev));
const medium = mapped.filter(c => c.sev === 'medium');
const merged = [...high, ...medium].slice(0, 5);
return merged.length >= 3 ? merged : null;
} catch {
return null;
}
}
async function getStats(req, res) {
try {
@@ -73,4 +141,20 @@ async function getStats(req, res) {
}
}
module.exports = { getStats };
async function getCVEs(req, res) {
const now = Date.now();
if (now - cveCache.ts < 3600000 && cveCache.data.length >= 3) {
return res.json({ status: 'success', data: cveCache.data, cached: true });
}
const fresh = await fetchCVEs();
if (fresh) {
cveCache = { ts: now, data: fresh };
return res.json({ status: 'success', data: fresh, cached: false });
}
if (cveCache.data.length >= 3) {
return res.json({ status: 'success', data: cveCache.data, cached: true });
}
res.status(503).json({ status: 'error', message: 'CVE-Feed nicht erreichbar' });
}
module.exports = { getStats, getCVEs };

View File

@@ -449,6 +449,8 @@ async function initializeDatabase() {
`ALTER TABLE monitoring_agents ADD COLUMN defender_enabled INTEGER DEFAULT 0`,
`ALTER TABLE monitoring_agents ADD COLUMN defender_signatures_age INTEGER DEFAULT -1`,
`ALTER TABLE monitoring_agents ADD COLUMN hardware_serial TEXT`,
// Pro-Geräte Agent-Key (Security-Migration weg vom geteilten AGENT_API_KEY)
`ALTER TABLE monitoring_agents ADD COLUMN agent_key TEXT`,
// Sichere Links (Shares)
`CREATE TABLE IF NOT EXISTS shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -481,6 +483,7 @@ async function initializeDatabase() {
`ALTER TABLE fido_keys ADD COLUMN last_used_at DATETIME`,
`ALTER TABLE fido_keys ADD COLUMN manufacturer TEXT`,
`ALTER TABLE fido_keys ADD COLUMN connection_type TEXT`,
`ALTER TABLE fido_keys ADD COLUMN pin TEXT`,
// Asset-Agent-Sync
`ALTER TABLE assets ADD COLUMN os TEXT`,
`ALTER TABLE assets ADD COLUMN ip_address TEXT`,
@@ -552,6 +555,23 @@ async function initializeDatabase() {
}
console.log('✅ Database migrations completed');
// Special migration: bestehende Klartext-PINs in fido_keys nachverschlüsseln (DSGVO Art. 32)
try {
const { encrypt } = require('../utils/crypto');
const plainPinRows = db.prepare(
`SELECT id, pin FROM fido_keys WHERE pin IS NOT NULL AND pin != '' AND instr(pin, ':') = 0`
).all();
if (plainPinRows.length > 0) {
const updatePin = db.prepare('UPDATE fido_keys SET pin = ? WHERE id = ?');
for (const row of plainPinRows) {
updatePin.run(encrypt(row.pin), row.id);
}
console.log(`🔐 ${plainPinRows.length} Klartext-PIN(s) in fido_keys nachverschlüsselt`);
}
} catch (e) {
console.error('⚠️ PIN-Verschlüsselungs-Migration fehlgeschlagen:', e.message);
}
// Special migration: rebuild network_devices to add SNMP + AP support
try {
const ndDef = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='network_devices'").get();

View File

@@ -1,6 +1,9 @@
const jwt = require('jsonwebtoken');
const JWT_CONFIG = require('../config/jwt');
// Endpunkte, die trotz erzwungenem Passwortwechsel erreichbar bleiben müssen
const PASSWORD_CHANGE_EXEMPT_PATHS = ['/api/auth/me', '/api/auth/change-password'];
/**
* Middleware to verify JWT token and attach user to request
*/
@@ -18,6 +21,19 @@ function authenticateToken(req, res, next) {
try {
const decoded = jwt.verify(token, JWT_CONFIG.secret);
req.user = decoded; // { id, username, email, role, roleId }
if (!PASSWORD_CHANGE_EXEMPT_PATHS.includes(req.originalUrl.split('?')[0])) {
const User = require('../models/User');
const dbUser = User.getById(decoded.id);
if (dbUser?.must_change_password) {
return res.status(403).json({
status: 'error',
code: 'PASSWORD_CHANGE_REQUIRED',
message: 'Passwortänderung erforderlich, bevor weitere Aktionen möglich sind'
});
}
}
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {

View File

@@ -105,6 +105,16 @@ class AuditLog {
return stmt.all(limit);
}
/**
* Löscht Audit-Log-Einträge älter als retentionDays (DSGVO Art. 5 Abs. 1 lit. e - Speicherbegrenzung)
*/
static cleanupOld(retentionDays = 180) {
const db = getDatabase();
const stmt = db.prepare(`DELETE FROM audit_log WHERE created_at < datetime('now', '-' || ? || ' days')`);
const result = stmt.run(retentionDays);
return result.changes;
}
/**
* Helper method to log user actions
*/

View File

@@ -1,4 +1,10 @@
const { getDatabase } = require('../config/database');
const { encrypt, decrypt } = require('../utils/crypto');
function withDecryptedPin(row) {
if (!row) return row;
return { ...row, pin: decrypt(row.pin) };
}
class FidoKey {
/**
@@ -19,7 +25,7 @@ class FidoKey {
LEFT JOIN users uu ON fk.updated_by_user_id = uu.id
ORDER BY fk.created_at DESC
`);
return stmt.all();
return stmt.all().map(withDecryptedPin);
}
/**
@@ -40,7 +46,7 @@ class FidoKey {
LEFT JOIN users uu ON fk.updated_by_user_id = uu.id
WHERE fk.id = ?
`);
return stmt.get(id);
return withDecryptedPin(stmt.get(id));
}
/**
@@ -58,7 +64,7 @@ class FidoKey {
LEFT JOIN users cu ON fk.created_by_user_id = cu.id
WHERE fk.serial_number = ?
`);
return stmt.get(serialNumber);
return withDecryptedPin(stmt.get(serialNumber));
}
/**
@@ -77,7 +83,7 @@ class FidoKey {
WHERE fk.status = ?
ORDER BY fk.created_at DESC
`);
return stmt.all(status);
return stmt.all(status).map(withDecryptedPin);
}
/**
@@ -94,7 +100,7 @@ class FidoKey {
WHERE fk.assigned_to_user_id = ?
ORDER BY fk.created_at DESC
`);
return stmt.all(userId);
return stmt.all(userId).map(withDecryptedPin);
}
/**
@@ -108,9 +114,10 @@ class FidoKey {
serial_number,
status,
description,
pin,
assigned_to_user_id,
created_by_user_id
) VALUES (?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
@@ -118,6 +125,7 @@ class FidoKey {
keyData.serial_number,
keyData.status,
keyData.description || null,
encrypt(keyData.pin) || null,
keyData.assigned_to_user_id || null,
keyData.created_by_user_id
);
@@ -150,6 +158,10 @@ class FidoKey {
fields.push('description = ?');
values.push(keyData.description);
}
if (keyData.pin !== undefined) {
fields.push('pin = ?');
values.push(encrypt(keyData.pin));
}
if (keyData.assigned_to_user_id !== undefined) {
fields.push('assigned_to_user_id = ?');
values.push(keyData.assigned_to_user_id);

View File

@@ -38,9 +38,14 @@ class OffboardingProtocol {
e.first_name as employee_first_name,
e.last_name as employee_last_name,
e.email as employee_email,
e.department as employee_department,
r.name as employee_role_name,
cu.username as created_by_username,
uu.username as updated_by_username
uu.username as updated_by_username,
(SELECT d.id FROM onboarding_departments d
WHERE LOWER(d.name) LIKE LOWER('%' || COALESCE(e.department,'') || '%')
AND COALESCE(e.department,'') != ''
ORDER BY d.id LIMIT 1) as employee_department_id
FROM offboarding_protocols op
INNER JOIN users e ON op.employee_user_id = e.id
INNER JOIN roles r ON e.role_id = r.id

View File

@@ -3,6 +3,7 @@ const router = express.Router();
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const { setAuthCookie } = require('../utils/authCookie');
const TENANT_ID = () => process.env.AZURE_TENANT_ID;
const CLIENT_ID = () => process.env.AZURE_CLIENT_ID;

View File

@@ -7,6 +7,9 @@ const { requireAdmin } = require('../middleware/roleCheck');
// Agent checkin — no JWT, uses X-Agent-Key header
router.post('/checkin', ctrl.checkin);
// Agent tauscht Bootstrap-Key gegen individuellen Per-Device-Key (Security-Migration)
router.post('/enroll', ctrl.enroll);
// Schneller Announcement-Poll (leichtgewichtig, alle 15 Sek)
router.post('/announcements-poll', ctrl.announcementsPoll);

View File

@@ -1,7 +1,16 @@
const express = require('express');
const router = express.Router();
const { getStats } = require('../controllers/tv.controller');
const { getStats, getCVEs } = require('../controllers/tv.controller');
router.get('/stats', getStats);
// Kein normaler Login (TV-Display im Büro) — aber ein Shared-Key statt komplett offen ins Netz.
function requireTvKey(req, res, next) {
if (!process.env.TV_DASHBOARD_KEY || req.query.key !== process.env.TV_DASHBOARD_KEY) {
return res.status(401).json({ status: 'error', message: 'Unauthorized' });
}
next();
}
router.get('/stats', requireTvKey, getStats);
router.get('/cves', requireTvKey, getCVEs);
module.exports = router;

View File

@@ -75,6 +75,16 @@ const authLimiter = rateLimit({
legacyHeaders: false
});
// Striktes Limit nur für /login — verhindert Brute-Force auf Passwörter
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 8,
message: { status: 'error', message: 'Zu viele Login-Versuche, bitte später erneut versuchen' },
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true
});
// ============================================================================
// ROUTES
// ============================================================================
@@ -127,6 +137,7 @@ app.get('/api/health/history', (req, res) => {
});
// API routes
app.use('/api/auth/login', loginLimiter);
app.use('/api/auth', authLimiter, authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/fido-keys', fidoKeyRoutes);
@@ -411,6 +422,12 @@ async function startServer() {
NetworkDevice.cleanupOldChecks();
});
// DSGVO-Speicherbegrenzung: alte patch_commands (inkl. Shell-Output) + audit_log (täglich 03:30)
cron.schedule('30 3 * * *', () => {
const { runRetentionCleanup } = require('./services/dataRetention.service');
runRetentionCleanup();
});
// Proxmox Monitoring (alle 5 Minuten)
if (process.env.PROXMOX_HOST && process.env.PROXMOX_TOKEN) {
const { pollProxmox } = require('./services/proxmoxService');

View File

@@ -0,0 +1,26 @@
const { getDatabase } = require('../config/database');
const AuditLog = require('../models/AuditLog');
// DSGVO Art. 5 Abs. 1 lit. e (Speicherbegrenzung) — Daten nur so lange aufbewahren wie nötig.
const PATCH_COMMANDS_RETENTION_DAYS = parseInt(process.env.PATCH_COMMANDS_RETENTION_DAYS || '90', 10);
const AUDIT_LOG_RETENTION_DAYS = parseInt(process.env.AUDIT_LOG_RETENTION_DAYS || '180', 10);
function cleanupPatchCommands() {
const db = getDatabase();
const stmt = db.prepare(
`DELETE FROM patch_commands WHERE created_at < datetime('now', '-' || ? || ' days') AND status IN ('done', 'failed')`
);
return stmt.run(PATCH_COMMANDS_RETENTION_DAYS).changes;
}
function runRetentionCleanup() {
try {
const patchDeleted = cleanupPatchCommands();
const auditDeleted = AuditLog.cleanupOld(AUDIT_LOG_RETENTION_DAYS);
console.log(`[DataRetention] Bereinigt: ${patchDeleted} patch_commands (>${PATCH_COMMANDS_RETENTION_DAYS}d), ${auditDeleted} audit_log Einträge (>${AUDIT_LOG_RETENTION_DAYS}d)`);
} catch (err) {
console.error('[DataRetention] Fehler:', err.message);
}
}
module.exports = { runRetentionCleanup, cleanupPatchCommands };

View File

@@ -431,9 +431,9 @@ async function sendTicketCreatedConfirmation(ticket) {
</p>
${infoCard([
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
['Betreff', `<strong style="color:#111827;">${ticket.title}</strong>`],
['Kategorie', `<span style="color:#374151;">${ticket.category}</span>`],
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${escHtml(ticket.ticket_number)}</strong>`],
['Betreff', `<strong style="color:#111827;">${escHtml(ticket.title)}</strong>`],
['Kategorie', `<span style="color:#374151;">${escHtml(ticket.category)}</span>`],
['Priorität', priorityBadge(ticket.priority)],
['Status', statusBadge(ticket.status)],
])}
@@ -494,11 +494,11 @@ async function sendTicketAssignedNotification(ticket, assignedUser) {
</p>
${infoCard([
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
['Betreff', `<strong style="color:#111827;">${ticket.title}</strong>`],
['Kategorie', `<span style="color:#374151;">${ticket.category}</span>`],
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${escHtml(ticket.ticket_number)}</strong>`],
['Betreff', `<strong style="color:#111827;">${escHtml(ticket.title)}</strong>`],
['Kategorie', `<span style="color:#374151;">${escHtml(ticket.category)}</span>`],
['Priorität', priorityBadge(ticket.priority)],
['Von', `<span style="color:#374151;">${ticket.requester_name || 'Unbekannt'}${ticket.requester_email ? ` &lt;${ticket.requester_email}&gt;` : ''}</span>`],
['Von', `<span style="color:#374151;">${escHtml(ticket.requester_name || 'Unbekannt')}${ticket.requester_email ? ` &lt;${escHtml(ticket.requester_email)}&gt;` : ''}</span>`],
])}`;
await sendMail(
@@ -554,10 +554,10 @@ async function sendCommentNotification(ticket, comment) {
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td bgcolor="#0d9488" style="background-color:#0d9488;border-radius:6px;width:28px;height:28px;text-align:center;vertical-align:middle;padding:0 8px;">
<span style="font-size:13px;font-weight:800;color:#ffffff;font-family:Inter,Helvetica,Arial,sans-serif;line-height:28px;">${authorName.charAt(0).toUpperCase()}</span>
<span style="font-size:13px;font-weight:800;color:#ffffff;font-family:Inter,Helvetica,Arial,sans-serif;line-height:28px;">${escHtml(authorName.charAt(0).toUpperCase())}</span>
</td>
<td style="padding-left:10px;vertical-align:middle;">
<span style="font-size:13px;font-weight:700;color:#111827;font-family:Inter,Helvetica,Arial,sans-serif;">${authorName}</span>
<span style="font-size:13px;font-weight:700;color:#111827;font-family:Inter,Helvetica,Arial,sans-serif;">${escHtml(authorName)}</span>
<span style="font-size:11px;color:#9ca3af;font-family:Inter,Helvetica,Arial,sans-serif;padding-left:6px;">· IT Support</span>
</td>
</tr>
@@ -567,14 +567,14 @@ async function sendCommentNotification(ticket, comment) {
<!-- Message body -->
<tr>
<td bgcolor="#ffffff" style="background-color:#ffffff;padding:16px;font-size:14px;color:#1f2937;line-height:1.75;white-space:pre-line;font-family:Inter,Helvetica,Arial,sans-serif;">
${comment.comment.replace(/</g, '&lt;').replace(/>/g, '&gt;')}
${escHtml(comment.comment)}
</td>
</tr>
</table>
${infoCard([
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
['Betreff', `<span style="color:#374151;">${ticket.title}</span>`],
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${escHtml(ticket.ticket_number)}</strong>`],
['Betreff', `<span style="color:#374151;">${escHtml(ticket.title)}</span>`],
['Status', statusBadge(ticket.status)],
])}`;
@@ -611,19 +611,19 @@ async function sendStaffCommentNotification(ticket, comment) {
const content = `
<h2 style="margin:0 0 8px;font-size:21px;font-weight:800;color:#111827;font-family:Inter,Helvetica,Arial,sans-serif;">Neue Antwort im Ticket</h2>
<p style="margin:0 0 20px;font-size:14px;color:#6b7280;line-height:1.7;font-family:Inter,Helvetica,Arial,sans-serif;">
<strong>${requesterName}</strong> hat auf Ticket <strong style="color:#0d9488;">${ticket.ticket_number}</strong> geantwortet.
<strong>${escHtml(requesterName)}</strong> hat auf Ticket <strong style="color:#0d9488;">${escHtml(ticket.ticket_number)}</strong> geantwortet.
</p>
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;margin-bottom:22px;">
<tr><td bgcolor="#f8fafc" style="background-color:#f8fafc;padding:10px 16px;border-bottom:1px solid #e5e7eb;">
<span style="font-size:13px;font-weight:700;color:#111827;font-family:Inter,Helvetica,Arial,sans-serif;">${requesterName}</span>
<span style="font-size:13px;font-weight:700;color:#111827;font-family:Inter,Helvetica,Arial,sans-serif;">${escHtml(requesterName)}</span>
</td></tr>
<tr><td bgcolor="#ffffff" style="background-color:#ffffff;padding:16px;font-size:14px;color:#1f2937;line-height:1.75;white-space:pre-line;font-family:Inter,Helvetica,Arial,sans-serif;">
${comment.comment.replace(/</g, '&lt;').replace(/>/g, '&gt;')}
${escHtml(comment.comment)}
</td></tr>
</table>
${infoCard([
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
['Betreff', `<span style="color:#374151;">${ticket.title}</span>`],
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${escHtml(ticket.ticket_number)}</strong>`],
['Betreff', `<span style="color:#374151;">${escHtml(ticket.title)}</span>`],
['Status', statusBadge(ticket.status)],
])}`;
@@ -660,10 +660,10 @@ async function sendStaffTicketCreatedNotification(ticket) {
const content = `
<h2 style="margin:0 0 8px;font-size:21px;font-weight:800;color:#111827;font-family:Inter,Helvetica,Arial,sans-serif;">Neues Ticket eingegangen</h2>
${infoCard([
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
['Betreff', `<strong style="color:#111827;">${ticket.title}</strong>`],
['Von', `<span style="color:#374151;">${ticket.requester_name || ''}${ticket.requester_email ? ` &lt;${ticket.requester_email}&gt;` : ''}</span>`],
['Kategorie', `<span style="color:#374151;">${ticket.category || '—'}</span>`],
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${escHtml(ticket.ticket_number)}</strong>`],
['Betreff', `<strong style="color:#111827;">${escHtml(ticket.title)}</strong>`],
['Von', `<span style="color:#374151;">${escHtml(ticket.requester_name || '')}${ticket.requester_email ? ` &lt;${escHtml(ticket.requester_email)}&gt;` : ''}</span>`],
['Kategorie', `<span style="color:#374151;">${escHtml(ticket.category || '—')}</span>`],
['Priorität', priorityBadge(ticket.priority)],
])}`;
@@ -735,8 +735,8 @@ async function sendStatusChangeNotification(ticket, oldStatus, newStatus) {
${statusChangeVisual}
${infoCard([
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
['Betreff', `<span style="color:#374151;">${ticket.title}</span>`],
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${escHtml(ticket.ticket_number)}</strong>`],
['Betreff', `<span style="color:#374151;">${escHtml(ticket.title)}</span>`],
['Priorität', priorityBadge(ticket.priority)],
])}
@@ -1032,10 +1032,10 @@ async function sendEscalationEmail(ticket) {
${introHtml}
</p>
<table width="100%" cellpadding="12" style="background:#fff8f8;border:1px solid #fca5a5;border-radius:8px;margin:0 0 16px;">
<tr><td><strong>Ticket:</strong> ${ticket.ticket_number}</td></tr>
<tr><td><strong>Titel:</strong> ${ticket.title}</td></tr>
<tr><td><strong>Ticket:</strong> ${escHtml(ticket.ticket_number)}</td></tr>
<tr><td><strong>Titel:</strong> ${escHtml(ticket.title)}</td></tr>
<tr><td><strong>Priorität:</strong> ${priorityBadge(ticket.priority)}</td></tr>
<tr><td><strong>Ersteller:</strong> ${ticket.requester_name || ticket.requester_email || 'Unbekannt'}</td></tr>
<tr><td><strong>Ersteller:</strong> ${escHtml(ticket.requester_name || ticket.requester_email || 'Unbekannt')}</td></tr>
<tr><td><strong>Erstellt:</strong> ${new Date(ticket.created_at + 'Z').toLocaleString('de-DE')}</td></tr>
</table>`;

View File

@@ -0,0 +1,18 @@
const { getDatabase } = require('../config/database');
// Übergangslogik pro-Geräte-Keys (DSGVO/Security-Migration):
// - Alte Agents (vor v2.8.0) schicken noch den einen geteilten AGENT_API_KEY → wird vorübergehend
// weiter akzeptiert, damit der Rollout nicht die ganze Fleet auf einmal bricht.
// - Migrierte Agents schicken ihren individuellen, beim Enrollment ausgestellten Key — dieser wird
// NUR für den exakt zugehörigen Hostname akzeptiert, ein gestohlener Key kann also nicht mehr
// benutzt werden, um sich als ein anderes Gerät auszugeben.
function validateAgentKey(presentedKey, hostname) {
if (!presentedKey) return false;
if (presentedKey === process.env.AGENT_API_KEY) return true;
if (!hostname) return false;
const db = getDatabase();
const row = db.prepare('SELECT agent_key FROM monitoring_agents WHERE hostname = ?').get(hostname);
return !!row?.agent_key && row.agent_key === presentedKey;
}
module.exports = { validateAgentKey };

View File

@@ -0,0 +1,20 @@
const COOKIE_NAME = 'token';
const isProd = process.env.NODE_ENV === 'production';
// httpOnly-Cookie statt Token in JS-lesbarem localStorage — verhindert dass ein XSS-Treffer
// das Session-Token einfach per document.cookie/localStorage ausliest.
function setAuthCookie(res, token, maxAgeMs = 8 * 60 * 60 * 1000) {
res.cookie(COOKIE_NAME, token, {
httpOnly: true,
secure: isProd,
sameSite: 'lax',
maxAge: maxAgeMs,
path: '/',
});
}
function clearAuthCookie(res) {
res.clearCookie(COOKIE_NAME, { httpOnly: true, secure: isProd, sameSite: 'lax', path: '/' });
}
module.exports = { setAuthCookie, clearAuthCookie, COOKIE_NAME };

View File

@@ -0,0 +1,34 @@
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
function getKey() {
const secret = process.env.ENCRYPTION_KEY || process.env.JWT_SECRET || 'itnexus-fallback-key';
return crypto.createHash('sha256').update(secret).digest();
}
function encrypt(plainText) {
if (plainText === null || plainText === undefined || plainText === '') return null;
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv);
const encrypted = Buffer.concat([cipher.update(String(plainText), 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted.toString('base64')}`;
}
function decrypt(cipherText) {
if (!cipherText) return null;
const parts = cipherText.split(':');
if (parts.length !== 3) return cipherText; // unverschlüsselter Altbestand
try {
const [ivB64, authTagB64, dataB64] = parts;
const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), Buffer.from(ivB64, 'base64'));
decipher.setAuthTag(Buffer.from(authTagB64, 'base64'));
const decrypted = Buffer.concat([decipher.update(Buffer.from(dataB64, 'base64')), decipher.final()]);
return decrypted.toString('utf8');
} catch {
return null;
}
}
module.exports = { encrypt, decrypt };

View File

@@ -0,0 +1,47 @@
const dns = require('dns').promises;
function isPrivateIp(ip) {
if (ip.includes(':')) {
// IPv6: loopback, link-local, unique-local
return ip === '::1' || /^fe80:/i.test(ip) || /^fc[0-9a-f]{2}:/i.test(ip) || /^fd[0-9a-f]{2}:/i.test(ip);
}
const parts = ip.split('.').map(Number);
if (parts.length !== 4 || parts.some(p => Number.isNaN(p))) return true; // unparsable → sicherheitshalber blocken
const [a, b] = parts;
if (a === 127) return true; // Loopback
if (a === 10) return true; // Private
if (a === 172 && b >= 16 && b <= 31) return true; // Private
if (a === 192 && b === 168) return true; // Private
if (a === 169 && b === 254) return true; // Link-local
if (a === 0) return true; // "this network"
return false;
}
// Wirft, falls die URL auf interne/private Adressen oder Loopback zeigt — verhindert SSRF
// über den Knowledge-Base-URL-Import (Server würde sonst beliebige interne Endpunkte abrufen).
async function assertPublicUrl(urlString) {
const parsed = new URL(urlString);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('Nur HTTP/HTTPS URLs erlaubt');
}
const hostname = parsed.hostname;
if (hostname === 'localhost' || hostname.endsWith('.local')) {
throw new Error('Interne/lokale Adressen sind nicht erlaubt');
}
let addresses;
try {
addresses = await dns.lookup(hostname, { all: true });
} catch {
throw new Error('Hostname konnte nicht aufgelöst werden');
}
for (const { address } of addresses) {
if (isPrivateIp(address)) {
throw new Error('Interne/private Adressen sind nicht erlaubt');
}
}
return parsed;
}
module.exports = { assertPublicUrl, isPrivateIp };

View File

@@ -1,6 +1,7 @@
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const { getDatabase } = require('../config/database');
const { validateAgentKey } = require('../utils/agentAuth');
// agentId -> WebSocket
const agentSockets = new Map();
@@ -43,12 +44,12 @@ function setupWebSocketServer(httpServer) {
function handleAgent(ws, url) {
const key = url.searchParams.get('key');
if (key !== process.env.AGENT_API_KEY) {
const hostname = url.searchParams.get('hostname');
if (!hostname) { ws.close(1008, 'hostname required'); return; }
if (!validateAgentKey(key, hostname)) {
ws.close(1008, 'unauthorized');
return;
}
const hostname = url.searchParams.get('hostname');
if (!hostname) { ws.close(1008, 'hostname required'); return; }
const db = getDatabase();
const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname);
@@ -90,29 +91,38 @@ function handleAgent(ws, url) {
});
}
// Token kommt NICHT mehr als URL-Query-Param (landet sonst im Klartext in nginx-Access-Logs),
// sondern als erste WS-Message ({type:'auth',token}) — erst danach wird die Verbindung freigeschaltet.
function handleBrowser(ws, url) {
const token = url.searchParams.get('token');
try {
jwt.verify(token, process.env.JWT_SECRET);
} catch {
ws.close(1008, 'unauthorized');
return;
}
const agentId = parseInt(url.searchParams.get('agentId'));
if (!agentId) { ws.close(1008, 'agentId required'); return; }
browserSockets.set(agentId, ws);
const aws = agentSockets.get(agentId);
if (aws?.readyState === WebSocket.OPEN) {
aws.send(JSON.stringify({ type: 'start_shell' }));
ws.send('\x1b[32m[Verbunden]\x1b[0m\r\n');
} else {
ws.send('\x1b[33m[Warte auf Agent-Verbindung...]\x1b[0m\r\n');
}
let authenticated = false;
const authTimer = setTimeout(() => { if (!authenticated) ws.close(1008, 'auth timeout'); }, 5000);
ws.on('message', (data) => {
if (!authenticated) {
clearTimeout(authTimer);
let decoded;
try {
const msg = JSON.parse(data.toString());
if (msg.type !== 'auth') throw new Error();
decoded = jwt.verify(msg.token, process.env.JWT_SECRET);
} catch { ws.close(1008, 'unauthorized'); return; }
if (!['admin', 'super_admin'].includes(decoded.role)) { ws.close(1008, 'forbidden'); return; }
authenticated = true;
browserSockets.set(agentId, ws);
const aws = agentSockets.get(agentId);
if (aws?.readyState === WebSocket.OPEN) {
aws.send(JSON.stringify({ type: 'start_shell' }));
ws.send('\x1b[32m[Verbunden]\x1b[0m\r\n');
} else {
ws.send('\x1b[33m[Warte auf Agent-Verbindung...]\x1b[0m\r\n');
}
return;
}
const aws = agentSockets.get(agentId);
if (!aws || aws.readyState !== WebSocket.OPEN) return;
// RTC-Signaling (offer/answer/ice) → transparent weiterleiten
@@ -134,9 +144,9 @@ function handleBrowser(ws, url) {
function handleRdpAgent(ws, url) {
const key = url.searchParams.get('key');
if (key !== process.env.AGENT_API_KEY) { ws.close(1008, 'unauthorized'); return; }
const hostname = url.searchParams.get('hostname');
if (!hostname) { ws.close(1008, 'hostname required'); return; }
if (!validateAgentKey(key, hostname)) { ws.close(1008, 'unauthorized'); return; }
const db = getDatabase();
const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname);
@@ -164,18 +174,30 @@ function handleRdpAgent(ws, url) {
}
function handleRdpBrowser(ws, url) {
const token = url.searchParams.get('token');
try { jwt.verify(token, process.env.JWT_SECRET); }
catch { ws.close(1008, 'unauthorized'); return; }
const agentId = parseInt(url.searchParams.get('agentId'));
if (!agentId) { ws.close(1008, 'agentId required'); return; }
const oldBws = rdpBrowserSockets.get(agentId);
if (oldBws?.readyState === WebSocket.OPEN) oldBws.close();
rdpBrowserSockets.set(agentId, ws);
let authenticated = false;
const authTimer = setTimeout(() => { if (!authenticated) ws.close(1008, 'auth timeout'); }, 5000);
ws.on('message', (data) => {
if (!authenticated) {
clearTimeout(authTimer);
let decoded;
try {
const msg = JSON.parse(data.toString());
if (msg.type !== 'auth') throw new Error();
decoded = jwt.verify(msg.token, process.env.JWT_SECRET);
} catch { ws.close(1008, 'unauthorized'); return; }
if (!['admin', 'super_admin'].includes(decoded.role)) { ws.close(1008, 'forbidden'); return; }
authenticated = true;
const oldBws = rdpBrowserSockets.get(agentId);
if (oldBws?.readyState === WebSocket.OPEN) oldBws.close();
rdpBrowserSockets.set(agentId, ws);
return;
}
const aws = rdpAgentSockets.get(agentId);
if (aws?.readyState === WebSocket.OPEN) aws.send(data.toString());
});

View File

@@ -1,749 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Benutzerverwaltung Design Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #3fa3a3;
--primary-dark: #008487;
--bg: #0f172a;
--bg2: #1e293b;
--bg3: #334155;
--text: #f1f5f9;
--text2: #94a3b8;
--text3: #64748b;
--border: #334155;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--radius: 10px;
--shadow: 0 4px 12px rgba(0,0,0,0.4);
}
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
/* ── Demo Selector Bar ── */
.demo-bar {
background: #0a1628;
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex; align-items: center; gap: 16px;
position: sticky; top: 0; z-index: 100;
}
.demo-bar-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--text3); }
.demo-tabs { display: flex; gap: 6px; }
.demo-tab {
padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500;
border: 1px solid var(--border); background: transparent; color: var(--text2);
cursor: pointer; transition: all 0.15s;
}
.demo-tab:hover { border-color: var(--primary); color: var(--primary); }
.demo-tab.active { background: var(--primary); border-color: var(--primary); color: #fff; }
.demo-tab.recommended::after { content: ' ★'; font-size: 11px; }
/* ── Page Container ── */
.page { display: none; padding: 28px 32px; max-width: 1300px; margin: 0 auto; }
.page.visible { display: block; }
/* ── Page Header ── */
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.page-title { font-size: 22px; font-weight: 700; }
.btn-group { display: flex; gap: 8px; }
.btn {
padding: 9px 18px; border-radius: 8px; font-size: 13px; font-weight: 500;
border: none; cursor: pointer; transition: all 0.15s; font-family: inherit;
}
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { background: var(--bg3); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--bg2); border-color: var(--primary); }
/* ── Stats Cards ── */
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 20px; }
.stat-card {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
padding: 16px 20px; display: flex; align-items: center; gap: 14px;
transition: border-color 0.15s;
}
.stat-card:hover { border-color: var(--primary); }
.stat-icon {
width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center;
justify-content: center; font-size: 18px; flex-shrink: 0;
}
.stat-icon.blue { background: rgba(59,130,246,0.15); }
.stat-icon.green { background: rgba(16,185,129,0.15); }
.stat-icon.red { background: rgba(239,68,68,0.15); }
.stat-icon.yellow { background: rgba(245,158,11,0.15); }
.stat-num { font-size: 24px; font-weight: 700; line-height: 1; }
.stat-label { font-size: 12px; color: var(--text2); margin-top: 3px; }
/* ── Filter Bar ── */
.filter-bar { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.search-input {
background: var(--bg2); border: 1px solid var(--border); color: var(--text);
border-radius: 8px; padding: 9px 14px; font-size: 13px; font-family: inherit;
outline: none; width: 280px; transition: border-color 0.15s;
}
.search-input:focus { border-color: var(--primary); }
.filter-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 500;
border: 1px solid var(--border); background: var(--bg2); color: var(--text2);
cursor: pointer; transition: all 0.12s; white-space: nowrap;
}
.chip:hover { border-color: var(--primary); color: var(--primary); }
.chip.active { background: var(--primary); border-color: var(--primary); color: #fff; }
.chip.active-red { background: var(--danger); border-color: var(--danger); color: #fff; }
.chip.active-green { background: var(--success); border-color: var(--success); color: #fff; }
.chip-sep { width: 1px; height: 24px; background: var(--border); margin: 0 2px; }
/* ── Table ── */
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
table { width: 100%; border-collapse: collapse; }
thead th {
background: var(--bg3); text-align: left; padding: 11px 16px;
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px;
color: var(--text3); border-bottom: 1px solid var(--border); white-space: nowrap;
}
tbody tr { border-bottom: 1px solid rgba(51,65,85,0.5); transition: background 0.12s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: rgba(63,163,163,0.04); }
td { padding: 12px 16px; font-size: 13px; vertical-align: middle; }
/* ── Avatar ── */
.user-cell { display: flex; align-items: center; gap: 10px; }
.avatar {
width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0;
color: #fff;
}
.user-name { font-weight: 500; font-size: 13px; }
.user-username { font-size: 11px; color: var(--text3); }
/* ── Badges ── */
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.4px;
}
.badge-dot { width: 6px; height: 6px; border-radius: 50%; }
.badge-active { background: rgba(16,185,129,0.15); color: var(--success); }
.badge-inactive { background: rgba(239,68,68,0.1); color: var(--danger); }
.role-badge {
padding: 3px 9px; border-radius: 6px; font-size: 11px; font-weight: 600; display: inline-flex; align-items: center; gap: 4px;
}
/* ── Action buttons (old style for Option A) ── */
.actions-old { display: flex; gap: 6px; }
.btn-sm { padding: 5px 12px; border-radius: 6px; font-size: 11px; font-weight: 600; border: none; cursor: pointer; font-family: inherit; }
.btn-sm-edit { background: rgba(63,163,163,0.15); color: var(--primary); }
.btn-sm-edit:hover { background: rgba(63,163,163,0.25); }
.btn-sm-toggle { background: rgba(148,163,184,0.1); color: var(--text2); }
.btn-sm-toggle:hover { background: rgba(148,163,184,0.2); }
.btn-sm-del { background: rgba(239,68,68,0.1); color: var(--danger); }
.btn-sm-del:hover { background: rgba(239,68,68,0.2); }
/* ── 3-dot menu (Option B+C) ── */
.menu-wrap { position: relative; display: inline-block; }
.menu-btn {
width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border);
background: transparent; color: var(--text2); cursor: pointer; font-size: 18px;
display: flex; align-items: center; justify-content: center; transition: all 0.12s;
line-height: 1;
}
.menu-btn:hover { background: var(--bg3); color: var(--text); border-color: var(--primary); }
.dropdown {
position: absolute; right: 0; top: calc(100% + 4px); background: var(--bg2);
border: 1px solid var(--border); border-radius: 8px; min-width: 160px;
box-shadow: var(--shadow); z-index: 50; overflow: hidden; display: none;
}
.dropdown.open { display: block; }
.dropdown-item {
padding: 9px 14px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px;
transition: background 0.1s; color: var(--text);
}
.dropdown-item:hover { background: var(--bg3); }
.dropdown-item.danger { color: var(--danger); }
.dropdown-item.danger:hover { background: rgba(239,68,68,0.1); }
.dropdown-sep { height: 1px; background: var(--border); margin: 3px 0; }
/* Inline role dropdown */
.role-select {
background: var(--bg3); border: 1px solid var(--border); color: var(--text);
border-radius: 6px; padding: 4px 8px; font-size: 12px; font-family: inherit;
cursor: pointer; outline: none; transition: border-color 0.12s;
}
.role-select:focus { border-color: var(--primary); }
/* ── Relative time ── */
.time-abs { font-size: 12px; color: var(--text2); }
.time-rel { font-size: 11px; color: var(--text3); }
.time-never { color: var(--text3); font-style: italic; }
/* ── Slideover (Option C) ── */
.slideover-layout { display: flex; gap: 0; position: relative; }
.table-area { flex: 1; min-width: 0; transition: margin-right 0.3s; }
.table-area.panel-open { margin-right: 0; }
.slideover {
width: 360px; flex-shrink: 0; background: var(--bg2); border: 1px solid var(--border);
border-radius: var(--radius); margin-left: 16px;
animation: slideIn 0.25s ease;
overflow-y: auto; max-height: calc(100vh - 200px);
}
@keyframes slideIn { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } }
.panel-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
.panel-avatar { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 700; color: #fff; flex-shrink: 0; }
.panel-name { font-weight: 700; font-size: 15px; }
.panel-email { font-size: 12px; color: var(--text2); }
.panel-close { margin-left: auto; background: none; border: none; color: var(--text2); font-size: 20px; cursor: pointer; padding: 4px; border-radius: 6px; }
.panel-close:hover { background: var(--bg3); color: var(--text); }
.panel-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
.panel-section:last-child { border-bottom: none; }
.panel-section-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--text3); margin-bottom: 12px; }
.panel-field { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 13px; }
.panel-field:last-child { margin-bottom: 0; }
.panel-field-label { color: var(--text2); }
.panel-actions { padding: 14px 20px; display: flex; gap: 8px; flex-direction: column; }
.btn-full { width: 100%; padding: 9px; text-align: center; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; border: none; font-family: inherit; transition: all 0.12s; }
.btn-full-primary { background: var(--primary); color: #fff; }
.btn-full-primary:hover { background: var(--primary-dark); }
.btn-full-warn { background: rgba(245,158,11,0.12); color: var(--warning); border: 1px solid rgba(245,158,11,0.3); }
.btn-full-warn:hover { background: rgba(245,158,11,0.2); }
.btn-full-danger { background: rgba(239,68,68,0.1); color: var(--danger); border: 1px solid rgba(239,68,68,0.25); }
.btn-full-danger:hover { background: rgba(239,68,68,0.18); }
/* Row clickable for C */
.clickable-row { cursor: pointer; }
.clickable-row.selected-row { background: rgba(63,163,163,0.08) !important; }
.clickable-row.selected-row td { border-left: 2px solid var(--primary); }
.clickable-row.selected-row td:not(:first-child) { border-left: none; }
/* ── Last login ── */
td .last-login { display: flex; flex-direction: column; }
/* ── Tooltip style note ── */
.design-note {
background: rgba(63,163,163,0.08); border: 1px solid rgba(63,163,163,0.2);
border-radius: 8px; padding: 10px 16px; margin-bottom: 16px;
font-size: 12px; color: var(--text2); line-height: 1.6;
}
.design-note strong { color: var(--primary); }
.combo-badge {
background: var(--primary); color: #fff; font-size: 10px; font-weight: 700;
padding: 2px 8px; border-radius: 20px; margin-left: 8px; text-transform: uppercase;
}
</style>
</head>
<body>
<!-- Demo Switcher Bar -->
<div class="demo-bar">
<span class="demo-bar-label">Design-Option:</span>
<div class="demo-tabs">
<button class="demo-tab" onclick="show('a')">A Stats + Filter-Chips</button>
<button class="demo-tab" onclick="show('b')">B Avatar + 3-Punkte-Menü</button>
<button class="demo-tab" onclick="show('c')">C Slideover-Panel</button>
<button class="demo-tab recommended active" onclick="show('combo')">Kombiniert (Empfehlung)</button>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
OPTION A Stats + Filter Chips
════════════════════════════════════════════════════════ -->
<div id="page-a" class="page">
<div class="page-header">
<h1 class="page-title">Benutzerverwaltung</h1>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Option A:</strong> Statistik-Kacheln oben + Filter-Chips statt nur Textsuche. Tabelle bleibt gleich. Kleinster Aufwand, sofortiger Informationsgewinn.
</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon blue">👥</div>
<div><div class="stat-num">14</div><div class="stat-label">Benutzer gesamt</div></div>
</div>
<div class="stat-card">
<div class="stat-icon green"></div>
<div><div class="stat-num">11</div><div class="stat-label">Aktiv</div></div>
</div>
<div class="stat-card">
<div class="stat-icon red">🚫</div>
<div><div class="stat-num">3</div><div class="stat-label">Deaktiviert</div></div>
</div>
<div class="stat-card">
<div class="stat-icon yellow">⏱️</div>
<div><div class="stat-num">4</div><div class="stat-label">Noch nie eingeloggt</div></div>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…" oninput="filterA(this.value)">
<div class="chip-sep"></div>
<div class="filter-chips" id="role-chips-a">
<span class="chip active" onclick="chipClick(this,'a')">Alle Rollen</span>
<span class="chip" onclick="chipClick(this,'a')">👑 Super Admin</span>
<span class="chip" onclick="chipClick(this,'a')">🛡️ Admin</span>
<span class="chip" onclick="chipClick(this,'a')">🎧 Support</span>
<span class="chip" onclick="chipClick(this,'a')">🔧 Bearbeiter</span>
<span class="chip" onclick="chipClick(this,'a')">👤 Benutzer</span>
</div>
<div class="chip-sep"></div>
<span class="chip active-green active" onclick="statusChip(this,'aktiv')">● Aktiv</span>
<span class="chip" onclick="statusChip(this,'inaktiv')">● Inaktiv</span>
</div>
<!-- Table (same as current, just with search) -->
<div class="card">
<table>
<thead>
<tr>
<th>Benutzername</th>
<th>E-Mail</th>
<th>Name</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="tbody-a"></tbody>
</table>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
OPTION B Avatar + 3-Punkte-Menü
════════════════════════════════════════════════════════ -->
<div id="page-b" class="page">
<div class="page-header">
<h1 class="page-title">Benutzerverwaltung</h1>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Option B:</strong> Avatar-Initialen in der ersten Spalte + Aktionen als 3-Punkte-Menü (spart viel Platz, besonders bei langen Buttons). Rolle inline per Dropdown änderbar.
</div>
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…">
</div>
<div class="card">
<table>
<thead>
<tr>
<th>Benutzer</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
<th style="width:50px">···</th>
</tr>
</thead>
<tbody id="tbody-b"></tbody>
</table>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
OPTION C Slideover-Panel
════════════════════════════════════════════════════════ -->
<div id="page-c" class="page">
<div class="page-header">
<h1 class="page-title">Benutzerverwaltung</h1>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Option C:</strong> Klick auf eine Zeile öffnet ein Detailpanel rechts. Tabelle bleibt links sichtbar. Kein Modal-Overlay. → <em>Klick auf eine Zeile zum Testen!</em>
</div>
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…">
</div>
<div class="slideover-layout" id="c-layout">
<div class="table-area card" id="c-table-area">
<table>
<thead>
<tr>
<th>Benutzer</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
</tr>
</thead>
<tbody id="tbody-c"></tbody>
</table>
</div>
<div class="slideover" id="c-panel" style="display:none"></div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
KOMBINATION (Empfehlung): Stats + Filter + Avatar + 3-Punkte + Slideover
════════════════════════════════════════════════════════ -->
<div id="page-combo" class="page visible">
<div class="page-header">
<div>
<h1 class="page-title">Benutzerverwaltung <span class="combo-badge">Empfehlung</span></h1>
</div>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Kombination aus A + B + C:</strong> Stats-Kacheln · Filter-Chips · Avatar-Initialen · 3-Punkte-Menü · Slideover-Panel bei Klick auf eine Zeile. → <em>Klick auf eine Zeile öffnet das Detail-Panel!</em>
</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon blue">👥</div>
<div><div class="stat-num">14</div><div class="stat-label">Benutzer gesamt</div></div>
</div>
<div class="stat-card">
<div class="stat-icon green"></div>
<div><div class="stat-num">11</div><div class="stat-label">Aktiv</div></div>
</div>
<div class="stat-card">
<div class="stat-icon red">🚫</div>
<div><div class="stat-num">3</div><div class="stat-label">Deaktiviert</div></div>
</div>
<div class="stat-card">
<div class="stat-icon yellow">⏱️</div>
<div><div class="stat-num">4</div><div class="stat-label">Noch nie eingeloggt</div></div>
</div>
</div>
<!-- Filter -->
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…">
<div class="chip-sep"></div>
<div class="filter-chips">
<span class="chip active" onclick="chipClick(this,'combo')">Alle Rollen</span>
<span class="chip" onclick="chipClick(this,'combo')">👑 Super Admin</span>
<span class="chip" onclick="chipClick(this,'combo')">🛡️ Admin</span>
<span class="chip" onclick="chipClick(this,'combo')">🎧 Support</span>
<span class="chip" onclick="chipClick(this,'combo')">👤 Benutzer</span>
</div>
<div class="chip-sep"></div>
<span class="chip active-green active">● Aktiv</span>
<span class="chip">● Inaktiv</span>
</div>
<div class="slideover-layout" id="combo-layout">
<div class="table-area card" id="combo-table-area">
<table>
<thead>
<tr>
<th>Benutzer</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
<th style="width:50px">···</th>
</tr>
</thead>
<tbody id="tbody-combo"></tbody>
</table>
</div>
<div class="slideover" id="combo-panel" style="display:none"></div>
</div>
</div>
<script>
// ── Mock Data ──
const ROLE_META = {
super_admin: { icon: '👑', color: '#ef4444', label: 'Super Admin', avatarBg: '#7f1d1d' },
admin: { icon: '🛡️', color: '#f97316', label: 'Admin', avatarBg: '#7c2d12' },
support: { icon: '🎧', color: '#3b82f6', label: 'IT-Support', avatarBg: '#1e3a8a' },
bearbeiter: { icon: '🔧', color: '#8b5cf6', label: 'Bearbeiter', avatarBg: '#4c1d95' },
benutzer: { icon: '👤', color: '#64748b', label: 'Benutzer', avatarBg: '#1e293b' },
hr_personal: { icon: '🧑‍💼', color: '#10b981', label: 'HR', avatarBg: '#064e3b' },
buchhaltung: { icon: '💶', color: '#a78bfa', label: 'Buchhaltung', avatarBg: '#3b0764' },
};
const AVATAR_COLORS = ['#1d4ed8','#0f766e','#b45309','#be185d','#7c3aed','#0369a1','#15803d','#c2410c'];
function avatarColor(name) {
let h = 0; for (let c of name) h = (h * 31 + c.charCodeAt(0)) & 0xffff;
return AVATAR_COLORS[h % AVATAR_COLORS.length];
}
function initials(u) {
if (u.first_name && u.last_name) return (u.first_name[0]+u.last_name[0]).toUpperCase();
return u.username.slice(0,2).toUpperCase();
}
const USERS = [
{ id:1, username:'gruessing', email:'gruessing@cereda-systems.de', first_name:'Simon', last_name:'Grüssing', role:'super_admin', active:true, last_login:'2026-05-27T14:32:00' },
{ id:2, username:'smueller', email:'smueller@cereda-systems.de', first_name:'Stefan', last_name:'Müller', role:'admin', active:true, last_login:'2026-05-26T09:11:00' },
{ id:3, username:'jschmidt', email:'jschmidt@cereda-systems.de', first_name:'Julia', last_name:'Schmidt', role:'support', active:true, last_login:'2026-05-27T08:45:00' },
{ id:4, username:'tbauer', email:'tbauer@cereda-systems.de', first_name:'Thomas', last_name:'Bauer', role:'support', active:true, last_login:'2026-05-25T16:20:00' },
{ id:5, username:'mweber', email:'mweber@cereda-systems.de', first_name:'Maria', last_name:'Weber', role:'bearbeiter', active:true, last_login:'2026-05-22T11:00:00' },
{ id:6, username:'fkoch', email:'fkoch@cereda-systems.de', first_name:'Felix', last_name:'Koch', role:'benutzer', active:true, last_login:'2026-05-20T09:00:00' },
{ id:7, username:'krichter', email:'krichter@cereda-systems.de', first_name:'Klaus', last_name:'Richter', role:'benutzer', active:true, last_login:'2026-05-15T14:00:00' },
{ id:8, username:'aschneider', email:'aschneider@cereda-systems.de', first_name:'Anna', last_name:'Schneider', role:'hr_personal', active:true, last_login:'2026-05-24T10:30:00' },
{ id:9, username:'benders', email:'benders@cereda-systems.de', first_name:'Björn', last_name:'Enders', role:'benutzer', active:true, last_login:'2026-05-19T07:55:00' },
{ id:10, username:'mfischer', email:'mfischer@cereda-systems.de', first_name:'Markus', last_name:'Fischer', role:'buchhaltung', active:true, last_login:'2026-05-23T13:00:00' },
{ id:11, username:'lbraun', email:'lbraun@cereda-systems.de', first_name:'Lena', last_name:'Braun', role:'benutzer', active:true, last_login:null },
{ id:12, username:'konferenzraum', email:'konferenzraum@cereda-systems.de', first_name:null, last_name:null, role:'benutzer', active:true, last_login:null },
{ id:13, username:'poolfahrzeug', email:'poolfahrzeug@cereda-systems.de', first_name:null, last_name:null, role:'benutzer', active:false, last_login:null },
{ id:14, username:'einkauf', email:'einkauf@cereda-systems.de', first_name:null, last_name:null, role:'benutzer', active:false, last_login:null },
];
function relativeTime(iso) {
if (!iso) return '<span class="time-never">Nie</span>';
const diff = Date.now() - new Date(iso).getTime();
const mins = Math.floor(diff/60000), hrs = Math.floor(mins/60), days = Math.floor(hrs/24);
let rel = days > 0 ? `vor ${days} Tag${days>1?'en':''}` : hrs > 0 ? `vor ${hrs} Std.` : mins > 0 ? `vor ${mins} Min.` : 'gerade eben';
const abs = new Date(iso).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'});
return `<div class="last-login"><span class="time-abs">${abs}</span><span class="time-rel">${rel}</span></div>`;
}
function roleBadge(role) {
const m = ROLE_META[role] || { icon:'🔵', color:'#64748b', label: role };
return `<span class="role-badge" style="background:${m.color}18;color:${m.color};">${m.icon} ${m.label}</span>`;
}
function statusBadge(active) {
return active
? `<span class="badge badge-active"><span class="badge-dot" style="background:var(--success)"></span>Aktiv</span>`
: `<span class="badge badge-inactive"><span class="badge-dot" style="background:var(--danger)"></span>Inaktiv</span>`;
}
function avatarHtml(u, size=34, fontSize=12) {
const bg = avatarColor(u.username);
const ini = initials(u);
return `<div class="avatar" style="width:${size}px;height:${size}px;background:${bg};font-size:${fontSize}px">${ini}</div>`;
}
// ── Option A ──
function renderA() {
const rows = USERS.map(u => `
<tr>
<td>${u.username}</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>${u.first_name || u.last_name ? (u.first_name||'') + ' ' + (u.last_name||'') : ''}</td>
<td>${roleBadge(u.role)}</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
<td>
<div class="actions-old">
<button class="btn-sm btn-sm-edit">Bearbeiten</button>
<button class="btn-sm btn-sm-toggle">${u.active?'Deaktivieren':'Aktivieren'}</button>
<button class="btn-sm btn-sm-del">Löschen</button>
</div>
</td>
</tr>
`).join('');
document.getElementById('tbody-a').innerHTML = rows;
}
// ── Option B ──
function renderB() {
const rows = USERS.map(u => `
<tr>
<td>
<div class="user-cell">
${avatarHtml(u)}
<div>
<div class="user-name">${u.first_name || u.last_name ? (u.first_name||'')+' '+(u.last_name||'') : u.username}</div>
<div class="user-username">@${u.username}</div>
</div>
</div>
</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>
<select class="role-select" title="Rolle direkt ändern">
${Object.entries(ROLE_META).map(([k,m]) => `<option value="${k}" ${k===u.role?'selected':''}>${m.icon} ${m.label}</option>`).join('')}
</select>
</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
<td>
<div class="menu-wrap" onclick="event.stopPropagation()">
<button class="menu-btn" onclick="toggleMenu(this)">⋯</button>
<div class="dropdown">
<div class="dropdown-item">✏️ Bearbeiten</div>
<div class="dropdown-item">${u.active ? '🚫 Deaktivieren' : '✅ Aktivieren'}</div>
<div class="dropdown-item">🔑 Passwort zurücksetzen</div>
<div class="dropdown-sep"></div>
<div class="dropdown-item danger">🗑️ Löschen</div>
</div>
</div>
</td>
</tr>
`).join('');
document.getElementById('tbody-b').innerHTML = rows;
}
// ── Option C Slideover ──
let selectedC = null;
function renderC() {
const rows = USERS.map((u,i) => `
<tr class="clickable-row" id="crow-${i}" onclick="openPanel('c', ${i})">
<td>
<div class="user-cell">
${avatarHtml(u)}
<div>
<div class="user-name">${u.first_name || u.last_name ? (u.first_name||'')+' '+(u.last_name||'') : u.username}</div>
<div class="user-username">@${u.username}</div>
</div>
</div>
</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>${roleBadge(u.role)}</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
</tr>
`).join('');
document.getElementById('tbody-c').innerHTML = rows;
}
// ── Combo ──
let selectedCombo = null;
function renderCombo() {
const rows = USERS.map((u,i) => `
<tr class="clickable-row" id="combrow-${i}" onclick="openPanel('combo', ${i})">
<td>
<div class="user-cell">
${avatarHtml(u)}
<div>
<div class="user-name">${u.first_name || u.last_name ? (u.first_name||'')+' '+(u.last_name||'') : u.username}</div>
<div class="user-username">@${u.username}</div>
</div>
</div>
</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>
<select class="role-select" onclick="event.stopPropagation()" title="Rolle direkt ändern">
${Object.entries(ROLE_META).map(([k,m]) => `<option value="${k}" ${k===u.role?'selected':''}>${m.icon} ${m.label}</option>`).join('')}
</select>
</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
<td>
<div class="menu-wrap" onclick="event.stopPropagation()">
<button class="menu-btn" onclick="toggleMenu(this)">⋯</button>
<div class="dropdown">
<div class="dropdown-item">✏️ Bearbeiten</div>
<div class="dropdown-item">${u.active ? '🚫 Deaktivieren' : '✅ Aktivieren'}</div>
<div class="dropdown-item">🔑 Passwort zurücksetzen</div>
<div class="dropdown-sep"></div>
<div class="dropdown-item danger">🗑️ Löschen</div>
</div>
</div>
</td>
</tr>
`).join('');
document.getElementById('tbody-combo').innerHTML = rows;
}
// ── Panel Content ──
function panelHtml(u, prefix) {
const m = ROLE_META[u.role] || {};
const displayName = u.first_name || u.last_name ? `${u.first_name||''} ${u.last_name||''}`.trim() : u.username;
return `
<div class="panel-header">
${avatarHtml(u, 48, 18)}
<div>
<div class="panel-name">${displayName}</div>
<div class="panel-email">${u.email}</div>
</div>
<button class="panel-close" onclick="closePanel('${prefix}')">×</button>
</div>
<div class="panel-section">
<div class="panel-section-label">Account</div>
<div class="panel-field"><span class="panel-field-label">Benutzername</span><span>@${u.username}</span></div>
<div class="panel-field"><span class="panel-field-label">Rolle</span>${roleBadge(u.role)}</div>
<div class="panel-field"><span class="panel-field-label">Status</span>${statusBadge(u.active)}</div>
</div>
<div class="panel-section">
<div class="panel-section-label">Aktivität</div>
<div class="panel-field"><span class="panel-field-label">Letzter Login</span>${u.last_login ? new Date(u.last_login).toLocaleString('de-DE') : '<span style="color:var(--text3);font-style:italic">Noch nie</span>'}</div>
<div class="panel-field"><span class="panel-field-label">Konto-ID</span><span style="color:var(--text3)">#${u.id}</span></div>
</div>
<div class="panel-actions">
<button class="btn-full btn-full-primary">✏️ Bearbeiten</button>
<button class="btn-full btn-full-warn">${u.active ? '🚫 Deaktivieren' : '✅ Aktivieren'}</button>
<button class="btn-full btn-full-danger">🗑️ Löschen</button>
</div>
`;
}
function openPanel(prefix, idx) {
const u = USERS[idx];
const panel = document.getElementById(`${prefix}-panel`);
panel.innerHTML = panelHtml(u, prefix);
panel.style.display = 'block';
// highlight row
document.querySelectorAll(`#tbody-${prefix} tr`).forEach((r,i) => r.classList.toggle('selected-row', i===idx));
}
function closePanel(prefix) {
document.getElementById(`${prefix}-panel`).style.display = 'none';
document.querySelectorAll(`#tbody-${prefix} tr`).forEach(r => r.classList.remove('selected-row'));
}
// ── Menu toggle ──
function toggleMenu(btn) {
const dd = btn.nextElementSibling;
const wasOpen = dd.classList.contains('open');
// close all
document.querySelectorAll('.dropdown.open').forEach(d => d.classList.remove('open'));
if (!wasOpen) dd.classList.add('open');
}
document.addEventListener('click', () => {
document.querySelectorAll('.dropdown.open').forEach(d => d.classList.remove('open'));
});
// ── Filter chips ──
function chipClick(el, page) {
el.closest('.filter-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
el.classList.add('active');
}
// ── Page switcher ──
function show(id) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('visible'));
document.getElementById('page-' + id).classList.add('visible');
document.querySelectorAll('.demo-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
}
// ── Init ──
renderA();
renderB();
renderC();
renderCombo();
</script>
</body>
</html>

View File

@@ -1,997 +0,0 @@
// IT Nexus Dev Team — Lokaler KI-Entwickler-Agent
// Start: node devteam.js
// Dann: http://localhost:4242
const http = require('http');
const https = require('https');
const API_KEY = process.env.ANTHROPIC_API_KEY || '';
const PORT = 4242;
// ─── IT Nexus Kontext für alle Agenten ────────────────────────────────────────
const IT_NEXUS_CONTEXT = `
# IT Nexus Vollständiger Stack-Kontext
## Tech Stack
- Backend: Node.js 18 + Express 4 + SQLite (better-sqlite3, SYNCHRON - kein await!)
- Frontend: React 18 (Create React App) + Custom CSS (Glassmorphism)
- Auth: JWT (jsonwebtoken) + bcryptjs
- KI: @anthropic-ai/sdk (Claude claude-sonnet-4-6)
- Deployment: Docker Compose + nginx auf LXC CT 102 (192.168.0.194)
## Dateistruktur Backend
\`\`\`
backend/src/
server.js Express Setup, alle app.use() Registrierungen
db/seed.js Alle DB-Migrations (jede in try/catch, idempotent!)
config/database.js SQLite Verbindung via better-sqlite3
middleware/
auth.js authenticateToken (JWT prüfen)
roleCheck.js requireAdmin, requireStaff Middleware
errorHandler.js asyncHandler(fn) Wrapper für alle Controller
controllers/ Business Logic, eine Datei pro Feature
models/ SQLite Queries, eine Datei pro Entity
routes/ Express Router, eine Datei pro Feature
services/ Externe APIs (Graph API, Proxmox, Anthropic)
\`\`\`
## Dateistruktur Frontend
\`\`\`
frontend/src/
pages/ Alle Seiten (NamingConvention: XxxPage.jsx)
components/common/
AppLayout.jsx Wrapper mit Sidebar + Topbar
Sidebar.jsx Navigation
Topbar.jsx Header
context/AuthContext.jsx Auth State, user Objekt
services/api.js Axios Instance mit Interceptors (IMMER verwenden!)
\`\`\`
## Kritische Konventionen Backend
1. DB-Migration: Jedes ALTER TABLE / CREATE TABLE einzeln in try/catch in seed.js
2. Controller immer mit asyncHandler wrappen: const foo = asyncHandler(async (req, res) => {...})
3. Statische Routen VOR /:id definieren (z.B. /statistics vor /:id)
4. better-sqlite3 ist SYNCHRON: db.prepare('...').get() KEIN await!
5. Exports am Ende: module.exports = { foo, bar }
## Kritische Konventionen Frontend
1. IMMER import api from '../../services/api' NIEMALS axios direkt importieren
2. IMMER user.role_name NIEMALS user.role (falsches Feld!)
3. CSS Variablen verwenden:
- Text: var(--text-primary), var(--text-secondary), var(--text-muted)
- Hintergrund: var(--bg-card), var(--bg-secondary), var(--border-color)
- Glassmorphism: backdrop-filter: blur(20px), rgba() Hintergründe
4. Rollen: super_admin, admin, support, bearbeiter, benutzer, hr_personal, buchhaltung
5. Neue Seite in AppLayout einbinden (Route in App.jsx)
## Deploy-Workflow
\`\`\`
scp datei.js root@192.168.0.194:/opt/it-nexus/backend/src/...
docker cp /opt/it-nexus/backend/src/.../datei.js fido-backend:/app/src/.../
docker restart fido-backend
# Frontend rebuild nur nötig wenn neue Seite/Komponente:
docker compose build frontend && docker compose up -d frontend
\`\`\`
## Beispiel: Neue Feature-Struktur
Backend: controller + model + route + seed.js Migration + server.js Route registrieren
Frontend: XxxPage.jsx + xxxService.js + Route in App.jsx + Link in Sidebar.jsx
`;
// ─── Agenten Definitionen ──────────────────────────────────────────────────────
const AGENTS = {
pm: {
name: 'Project Manager',
emoji: '📋',
color: '#6366f1',
system: `Du bist der Project Manager des IT Nexus Entwicklerteams. Du koordinierst alle anderen Agenten.
${IT_NEXUS_CONTEXT}
Deine Aufgabe:
1. Analysiere die Anfrage des Benutzers
2. Erstelle einen klaren Implementierungsplan
3. Bestimme welche Spezialisten benötigt werden
4. Fasse am Ende alles zusammen
Antworte auf Deutsch. Sei konkret und strukturiert. Nutze Markdown.
Erkläre kurz was getan werden muss, dann liste die benötigten Schritte auf.`
},
architect: {
name: 'Architect',
emoji: '🏗️',
color: '#f59e0b',
system: `Du bist der System-Architekt des IT Nexus Entwicklerteams.
${IT_NEXUS_CONTEXT}
Deine Aufgabe:
- Entscheide über DB-Schema (SQLite Spalten, Typen, Constraints)
- Entscheide über API-Struktur (Endpoints, HTTP-Methoden, Auth-Level)
- Entscheide über Datenfluss zwischen Frontend und Backend
- Identifiziere Abhängigkeiten zu bestehenden Features
Antworte auf Deutsch. Gib konkrete technische Entscheidungen.
Format: DB-Schema → API-Endpoints → Abhängigkeiten → Besonderheiten`
},
backend: {
name: 'Backend Dev',
emoji: '⚙️',
color: '#10b981',
system: `Du bist der Backend-Entwickler des IT Nexus Entwicklerteams. Du schreibst den kompletten Backend-Code.
${IT_NEXUS_CONTEXT}
Deine Aufgabe:
- Schreibe vollständige, lauffähige Node.js Dateien
- Folge exakt den Konventionen (asyncHandler, better-sqlite3 synchron, etc.)
- Jede Datei vollständig mit korrekten Imports und Exports
- DB-Migrations in seed.js Format
Format pro Datei:
### Datei: \`backend/src/pfad/dateiname.js\`
\`\`\`javascript
// kompletter Code
\`\`\`
Antworte auf Deutsch für Erklärungen, Code auf Englisch.`
},
frontend: {
name: 'Frontend Dev',
emoji: '🎨',
color: '#3b82f6',
system: `Du bist der Frontend-Entwickler des IT Nexus Entwicklerteams. Du schreibst den kompletten Frontend-Code.
${IT_NEXUS_CONTEXT}
Deine Aufgabe:
- Schreibe vollständige React Komponenten (.jsx Dateien)
- Glassmorphism Design (konsistent mit bestehendem IT Nexus Style)
- IMMER api.get/post aus services/api.js verwenden
- IMMER user.role_name verwenden
- CSS inline mit var(--*) Variablen
Format pro Datei:
### Datei: \`frontend/src/pfad/DateiName.jsx\`
\`\`\`jsx
// kompletter Code
\`\`\`
Antworte auf Deutsch für Erklärungen, Code auf Englisch.`
},
senior: {
name: 'Senior Dev',
emoji: '🔍',
color: '#ef4444',
system: `Du bist der Senior Developer und Code Reviewer des IT Nexus Entwicklerteams.
${IT_NEXUS_CONTEXT}
Deine Aufgabe:
- Reviewe den generierten Code auf Bugs, Sicherheitsprobleme, Konventionsverletzungen
- Prüfe ob alle Konventionen eingehalten wurden (user.role_name, api.js, asyncHandler, etc.)
- Prüfe auf fehlende Error Handling
- Prüfe auf fehlende Route-Registrierungen in server.js
- Prüfe auf fehlende Sidebar-Links
- Gib konkrete Fixes wenn nötig
Format:
✅ Was gut ist
⚠️ Was fehlt / falsch ist + Fix
📝 Deploy-Befehl am Ende
Antworte auf Deutsch.`
},
qa: {
name: 'QA Engineer',
emoji: '🧪',
color: '#8b5cf6',
system: `Du bist der QA Engineer des IT Nexus Entwicklerteams.
${IT_NEXUS_CONTEXT}
Deine Aufgabe:
- Identifiziere potenzielle Edge Cases
- Schreibe manuelle Testschritte die der Entwickler ausführen soll
- Prüfe ob alle API-Endpoints abgesichert sind (Auth)
- Prüfe Fehlerszenarien (leere Daten, falscher Input, fehlende Rechte)
- Schreibe wenn möglich einen Playwright-Test
Format:
📋 Manuelle Tests (nummeriert)
🔒 Security-Checks
⚡ Edge Cases
(Optional) Playwright Test Code
Antworte auf Deutsch.`
}
};
// ─── Anthropic API Aufruf ──────────────────────────────────────────────────────
function callClaude(systemPrompt, messages, onChunk) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: 8096,
system: systemPrompt,
messages,
stream: true
});
const req = https.request({
hostname: 'api.anthropic.com',
path: '/v1/messages',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01',
'Content-Length': Buffer.byteLength(body)
}
}, (res) => {
let full = '';
res.on('data', chunk => {
const lines = chunk.toString().split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const d = JSON.parse(line.slice(6));
if (d.type === 'content_block_delta' && d.delta?.text) {
full += d.delta.text;
onChunk(d.delta.text);
}
} catch {}
}
}
});
res.on('end', () => resolve(full));
res.on('error', reject);
});
req.on('error', reject);
req.write(body);
req.end();
});
}
// ─── Orchestrierung ────────────────────────────────────────────────────────────
async function runTeam(task, send) {
const context = [];
const runAgent = async (agentId, userMsg) => {
const agent = AGENTS[agentId];
send({ type: 'agent_start', agent: agentId, name: agent.name, emoji: agent.emoji, color: agent.color });
const msgs = [...context, { role: 'user', content: userMsg }];
let full = '';
await callClaude(agent.system, msgs, chunk => {
full += chunk;
send({ type: 'chunk', agent: agentId, text: chunk });
});
send({ type: 'agent_done', agent: agentId });
context.push({ role: 'user', content: userMsg });
context.push({ role: 'assistant', content: full });
return full;
};
// 1. PM analysiert
const pmResult = await runAgent('pm',
`Neue Aufgabe für das IT Nexus Entwicklerteam:\n\n"${task}"\n\nErstelle einen kurzen Implementierungsplan.`
);
// 2. Architect entscheidet
await runAgent('architect',
`Aufgabe: "${task}"\n\nPM-Plan:\n${pmResult}\n\nGib deine technischen Entscheidungen für DB-Schema und API-Struktur.`
);
// 3. Backend Dev schreibt Code
const backendResult = await runAgent('backend',
`Aufgabe: "${task}"\n\nSchreibe den vollständigen Backend-Code (Controller, Model, Route, seed.js Migration). Alle Dateien komplett.`
);
// 4. Frontend Dev schreibt Code
const frontendResult = await runAgent('frontend',
`Aufgabe: "${task}"\n\nSchreibe den vollständigen Frontend-Code (Page, Service). Alle Dateien komplett.`
);
// 5. Senior Dev reviewt
await runAgent('senior',
`Reviewe diesen Code:\n\n**Backend:**\n${backendResult}\n\n**Frontend:**\n${frontendResult}\n\nFinde Bugs, Konventionsverletzungen, fehlende Teile.`
);
// 6. QA testet
await runAgent('qa',
`Aufgabe: "${task}"\n\nErstelle Testschritte und prüfe Edge Cases für die implementierte Funktion.`
);
send({ type: 'done' });
}
// ─── HTTP Server ───────────────────────────────────────────────────────────────
const HTML = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Nexus Dev Team</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #0a0f1e;
color: #e2e8f0;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
.header {
background: rgba(15,23,42,0.95);
border-bottom: 1px solid rgba(99,102,241,0.3);
padding: 14px 24px;
display: flex;
align-items: center;
gap: 14px;
flex-shrink: 0;
}
.header-logo {
width: 36px; height: 36px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
}
.header h1 { font-size: 18px; font-weight: 700; color: #fff; }
.header p { font-size: 12px; color: #64748b; margin-top: 1px; }
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #10b981; margin-left: auto;
box-shadow: 0 0 8px #10b981;
animation: pulse 2s infinite;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
/* Layout */
.main { display: flex; flex: 1; overflow: hidden; }
/* Sidebar: Team */
.sidebar {
width: 200px;
background: rgba(15,23,42,0.8);
border-right: 1px solid rgba(255,255,255,0.06);
padding: 16px 12px;
display: flex;
flex-direction: column;
gap: 8px;
flex-shrink: 0;
}
.sidebar-title {
font-size: 10px;
font-weight: 700;
color: #475569;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
padding: 0 4px;
}
.agent-card {
padding: 8px 10px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
color: #94a3b8;
border: 1px solid transparent;
transition: all 0.2s;
}
.agent-card.active {
background: rgba(255,255,255,0.06);
color: #fff;
border-color: rgba(255,255,255,0.1);
}
.agent-card.thinking {
animation: agent-pulse 0.8s infinite;
}
@keyframes agent-pulse {
0%,100% { opacity: 1; }
50% { opacity: 0.5; }
}
.agent-emoji { font-size: 16px; }
.agent-status {
width: 6px; height: 6px; border-radius: 50%;
background: #1e293b;
margin-left: auto;
flex-shrink: 0;
}
.agent-status.active { background: #10b981; box-shadow: 0 0 6px #10b981; }
.agent-status.done { background: #6366f1; }
/* Chat Area */
.chat { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.chat::-webkit-scrollbar { width: 4px; }
.chat::-webkit-scrollbar-track { background: transparent; }
.chat::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
.msg { display: flex; gap: 12px; }
.msg-avatar {
width: 36px; height: 36px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
flex-shrink: 0;
border: 1px solid rgba(255,255,255,0.1);
}
.msg-body { flex: 1; min-width: 0; }
.msg-header {
display: flex; align-items: center; gap: 8px;
margin-bottom: 6px;
}
.msg-name { font-size: 13px; font-weight: 700; }
.msg-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.msg-content {
font-size: 13px;
line-height: 1.7;
color: #cbd5e1;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 10px;
padding: 14px 16px;
white-space: pre-wrap;
word-break: break-word;
}
/* Code Blocks */
.msg-content pre {
background: rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 14px;
margin: 10px 0;
overflow-x: auto;
font-family: 'Cascadia Code', 'Fira Code', Consolas, monospace;
font-size: 12px;
line-height: 1.6;
}
.msg-content code {
background: rgba(0,0,0,0.4);
padding: 1px 5px;
border-radius: 3px;
font-family: 'Cascadia Code', Consolas, monospace;
font-size: 12px;
color: #7dd3fc;
}
.msg-content pre code {
background: none; padding: 0; color: #e2e8f0;
}
.msg-content h1, .msg-content h2, .msg-content h3 {
color: #f1f5f9; margin: 14px 0 6px;
}
.msg-content ul, .msg-content ol { padding-left: 20px; margin: 6px 0; }
.msg-content li { margin: 3px 0; }
.msg-content strong { color: #f1f5f9; }
/* User message */
.msg.user .msg-content {
background: rgba(99,102,241,0.1);
border-color: rgba(99,102,241,0.3);
color: #e2e8f0;
}
/* Input Area */
.input-area {
border-top: 1px solid rgba(255,255,255,0.06);
background: rgba(15,23,42,0.95);
padding: 16px 20px;
flex-shrink: 0;
}
.api-key-bar {
display: flex; gap: 8px; margin-bottom: 10px; align-items: center;
}
.api-key-bar label { font-size: 11px; color: #475569; white-space: nowrap; }
.api-key-bar input {
flex: 1;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
padding: 6px 10px;
color: #94a3b8;
font-size: 12px;
font-family: monospace;
}
.input-row { display: flex; gap: 10px; }
textarea {
flex: 1;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 10px;
padding: 12px 14px;
color: #e2e8f0;
font-size: 13px;
resize: none;
font-family: inherit;
line-height: 1.5;
transition: border-color 0.2s;
}
textarea:focus { outline: none; border-color: rgba(99,102,241,0.5); }
textarea::placeholder { color: #334155; }
.send-btn {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border: none;
border-radius: 10px;
color: #fff;
font-size: 13px;
font-weight: 700;
padding: 0 20px;
cursor: pointer;
transition: opacity 0.2s;
white-space: nowrap;
}
.send-btn:hover { opacity: 0.85; }
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.hint { font-size: 11px; color: #334155; margin-top: 8px; }
/* Welcome */
.welcome {
margin: auto;
text-align: center;
padding: 40px;
max-width: 500px;
}
.welcome .big-emoji { font-size: 60px; margin-bottom: 16px; }
.welcome h2 { font-size: 22px; font-weight: 700; color: #f1f5f9; margin-bottom: 8px; }
.welcome p { font-size: 14px; color: #475569; line-height: 1.6; }
.examples { margin-top: 24px; display: flex; flex-direction: column; gap: 8px; }
.example-btn {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 10px 14px;
color: #94a3b8;
font-size: 12px;
cursor: pointer;
text-align: left;
transition: all 0.2s;
}
.example-btn:hover { background: rgba(255,255,255,0.08); color: #e2e8f0; }
</style>
</head>
<body>
<div class="header">
<div class="header-logo">👨‍💻</div>
<div>
<h1>IT Nexus Dev Team</h1>
<p>6 KI-Agenten · Lokal · Nur für dich</p>
</div>
<div class="status-dot"></div>
</div>
<div class="main">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-title">Dev Team</div>
<div class="agent-card" id="card-pm">
<span class="agent-emoji">📋</span>
<span>Project Manager</span>
<div class="agent-status" id="status-pm"></div>
</div>
<div class="agent-card" id="card-architect">
<span class="agent-emoji">🏗️</span>
<span>Architect</span>
<div class="agent-status" id="status-architect"></div>
</div>
<div class="agent-card" id="card-backend">
<span class="agent-emoji">⚙️</span>
<span>Backend Dev</span>
<div class="agent-status" id="status-backend"></div>
</div>
<div class="agent-card" id="card-frontend">
<span class="agent-emoji">🎨</span>
<span>Frontend Dev</span>
<div class="agent-status" id="status-frontend"></div>
</div>
<div class="agent-card" id="card-senior">
<span class="agent-emoji">🔍</span>
<span>Senior Dev</span>
<div class="agent-status" id="status-senior"></div>
</div>
<div class="agent-card" id="card-qa">
<span class="agent-emoji">🧪</span>
<span>QA Engineer</span>
<div class="agent-status" id="status-qa"></div>
</div>
</div>
<!-- Chat -->
<div class="chat" id="chat">
<div class="welcome" id="welcome">
<div class="big-emoji">🚀</div>
<h2>Dein persönliches Dev Team</h2>
<p>Beschreibe was du bauen möchtest. Das Team analysiert, plant, schreibt Code und reviewt alles automatisch.</p>
<div class="examples">
<button class="example-btn" onclick="setExample(this)">📦 Neue Seite für Lizenzmanagement (CRUD, DB, API, React)</button>
<button class="example-btn" onclick="setExample(this)">🔔 E-Mail-Benachrichtigung wenn Agent offline ist</button>
<button class="example-btn" onclick="setExample(this)">📊 Dashboard-Widget mit Top 5 offenen Tickets</button>
<button class="example-btn" onclick="setExample(this)">🔐 Zwei-Faktor-Authentifizierung für Admin-Login</button>
</div>
</div>
</div>
</div>
<!-- Input -->
<div class="input-area">
<div class="api-key-bar">
<label>🔑 API Key:</label>
<input type="password" id="apiKey" placeholder="sk-ant-..." oninput="saveKey(this.value)" />
</div>
<div class="input-row">
<textarea id="taskInput" rows="2" placeholder="Was soll das Team bauen? (Enter = Senden, Shift+Enter = Neue Zeile)"></textarea>
<button class="send-btn" id="sendBtn" onclick="sendTask()">Team starten ▶</button>
</div>
<div class="hint">⚡ PM → Architect → Backend Dev → Frontend Dev → Senior Dev → QA Engineer</div>
</div>
<script>
const COLORS = {
pm: '#6366f1', architect: '#f59e0b', backend: '#10b981',
frontend: '#3b82f6', senior: '#ef4444', qa: '#8b5cf6'
};
const NAMES = {
pm: 'Project Manager', architect: 'Architect', backend: 'Backend Dev',
frontend: 'Frontend Dev', senior: 'Senior Dev', qa: 'QA Engineer'
};
const EMOJIS = {
pm: '📋', architect: '🏗️', backend: '⚙️',
frontend: '🎨', senior: '🔍', qa: '🧪'
};
// API Key aus localStorage laden
window.onload = () => {
const saved = localStorage.getItem('devteam_apikey');
if (saved) document.getElementById('apiKey').value = saved;
};
function saveKey(v) { localStorage.setItem('devteam_apikey', v); }
function setExample(btn) {
document.getElementById('taskInput').value = btn.textContent.replace(/^[^\s]+\s/, '').trim();
document.getElementById('taskInput').focus();
}
document.getElementById('taskInput').addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendTask(); }
});
let currentMsgEl = null;
let currentRaw = '';
function addUserMsg(text) {
document.getElementById('welcome')?.remove();
const el = document.createElement('div');
el.className = 'msg user';
el.innerHTML = \`
<div class="msg-avatar" style="background:rgba(99,102,241,0.2)">👤</div>
<div class="msg-body">
<div class="msg-header">
<span class="msg-name" style="color:#6366f1">Du</span>
</div>
<div class="msg-content">\${escHtml(text)}</div>
</div>\`;
document.getElementById('chat').appendChild(el);
scrollChat();
}
function startAgentMsg(agentId) {
// Sidebar aktualisieren
document.querySelectorAll('.agent-card').forEach(c => c.classList.remove('active','thinking'));
document.querySelectorAll('.agent-status').forEach(s => { s.classList.remove('active'); });
const card = document.getElementById('card-' + agentId);
const status = document.getElementById('status-' + agentId);
if (card) { card.classList.add('active','thinking'); }
if (status) { status.classList.add('active'); }
const color = COLORS[agentId];
const name = NAMES[agentId];
const emoji = EMOJIS[agentId];
const el = document.createElement('div');
el.className = 'msg';
el.id = 'msg-' + agentId;
el.innerHTML = \`
<div class="msg-avatar" style="background:\${color}20; border-color:\${color}40">\${emoji}</div>
<div class="msg-body">
<div class="msg-header">
<span class="msg-name" style="color:\${color}">\${name}</span>
<span class="msg-badge" style="background:\${color}20;color:\${color}">schreibt...</span>
</div>
<div class="msg-content" id="content-\${agentId}"><span class="cursor">▌</span></div>
</div>\`;
document.getElementById('chat').appendChild(el);
currentMsgEl = document.getElementById('content-' + agentId);
currentRaw = '';
scrollChat();
}
function appendChunk(agentId, text) {
currentRaw += text;
if (currentMsgEl) {
currentMsgEl.innerHTML = renderMarkdown(currentRaw) + '<span class="cursor">▌</span>';
scrollChat();
}
}
function doneAgentMsg(agentId) {
if (currentMsgEl) {
currentMsgEl.innerHTML = renderMarkdown(currentRaw);
}
const card = document.getElementById('card-' + agentId);
const status = document.getElementById('status-' + agentId);
if (card) card.classList.remove('thinking');
if (status) { status.classList.remove('active'); status.classList.add('done'); }
// Badge aktualisieren
const msg = document.getElementById('msg-' + agentId);
if (msg) {
const badge = msg.querySelector('.msg-badge');
if (badge) { badge.textContent = 'fertig ✓'; badge.style.background = COLORS[agentId]+'30'; }
}
currentMsgEl = null;
}
function renderMarkdown(text) {
return text
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/\`\`\`(\w+)?\n([\s\S]*?)\`\`\`/g, '<pre><code>$2</code></pre>')
.replace(/\`([^\`]+)\`/g, '<code>$1</code>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/^(\d+)\. (.+)$/gm, '<li>$1. $2</li>')
.replace(/\n\n/g, '<br><br>')
.replace(/\n/g, '<br>');
}
function escHtml(t) {
return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function scrollChat() {
const chat = document.getElementById('chat');
chat.scrollTop = chat.scrollHeight;
}
async function sendTask() {
const task = document.getElementById('taskInput').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
if (!task) return;
if (!apiKey) { alert('Bitte zuerst den Anthropic API Key eingeben!'); return; }
const btn = document.getElementById('sendBtn');
btn.disabled = true;
btn.textContent = '⏳ Team arbeitet...';
addUserMsg(task);
document.getElementById('taskInput').value = '';
// Alle Status zurücksetzen
document.querySelectorAll('.agent-status').forEach(s => s.classList.remove('active','done'));
document.querySelectorAll('.agent-card').forEach(c => c.classList.remove('active','thinking'));
try {
const res = await fetch('/api/task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task, apiKey })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n\n');
buf = lines.pop();
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const d = JSON.parse(line.slice(6));
if (d.type === 'agent_start') startAgentMsg(d.agent);
else if (d.type === 'chunk') appendChunk(d.agent, d.text);
else if (d.type === 'agent_done') doneAgentMsg(d.agent);
else if (d.type === 'done') {
document.querySelectorAll('.agent-card').forEach(c => c.classList.remove('active','thinking'));
}
} catch {}
}
}
}
} catch (e) {
const el = document.createElement('div');
el.style.cssText = 'background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:8px;padding:12px;color:#fca5a5;font-size:13px;';
el.textContent = '❌ Fehler: ' + e.message;
document.getElementById('chat').appendChild(el);
}
btn.disabled = false;
btn.textContent = 'Team starten ▶';
scrollChat();
}
</script>
</body>
</html>`;
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
// HTML
if (req.method === 'GET' && url.pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(HTML);
return;
}
// Task API
if (req.method === 'POST' && url.pathname === '/api/task') {
let body = '';
req.on('data', c => body += c);
req.on('end', async () => {
try {
const { task, apiKey } = JSON.parse(body);
// API Key aus Request oder Umgebungsvariable
const key = apiKey || API_KEY;
if (!key) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Kein API Key' }));
return;
}
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
// Agenten mit aktuellem Key aufrufen
const callAgent = (systemPrompt, messages) => new Promise((resolve, reject) => {
const reqBody = JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: 8096,
system: systemPrompt,
messages,
stream: true
});
const apiReq = https.request({
hostname: 'api.anthropic.com',
path: '/v1/messages',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': key,
'anthropic-version': '2023-06-01',
'Content-Length': Buffer.byteLength(reqBody)
}
}, (apiRes) => {
let full = '';
let buf = '';
apiRes.on('data', chunk => {
buf += chunk.toString();
const lines = buf.split('\n');
buf = lines.pop();
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const d = JSON.parse(line.slice(6));
if (d.type === 'content_block_delta' && d.delta?.text) {
full += d.delta.text;
send({ type: 'chunk', agent: currentAgent, text: d.delta.text });
}
} catch {}
}
}
});
apiRes.on('end', () => resolve(full));
apiRes.on('error', reject);
});
apiReq.on('error', reject);
apiReq.write(reqBody);
apiReq.end();
});
let currentAgent = '';
const context = [];
const runAgent = async (agentId, userMsg) => {
currentAgent = agentId;
send({ type: 'agent_start', agent: agentId });
const msgs = [...context, { role: 'user', content: userMsg }];
const full = await callAgent(AGENTS[agentId].system, msgs);
send({ type: 'agent_done', agent: agentId });
context.push({ role: 'user', content: userMsg });
context.push({ role: 'assistant', content: full });
return full;
};
// Team ausführen
const pmResult = await runAgent('pm',
`Neue Aufgabe:\n\n"${task}"\n\nErstelle einen kurzen klaren Implementierungsplan für das IT Nexus Entwicklerteam.`
);
await runAgent('architect',
`Aufgabe: "${task}"\n\nPM-Plan:\n${pmResult}\n\nEntscheide über DB-Schema, API-Endpoints und technische Architektur.`
);
const backendResult = await runAgent('backend',
`Aufgabe: "${task}"\n\nSchreibe jetzt den vollständigen Backend-Code. Alle Dateien komplett und lauffähig. Folge exakt den IT Nexus Konventionen.`
);
const frontendResult = await runAgent('frontend',
`Aufgabe: "${task}"\n\nSchreibe jetzt den vollständigen Frontend-Code. Alle Dateien komplett. Folge exakt den IT Nexus Konventionen.`
);
await runAgent('senior',
`Reviewe diesen Code:\n\nBACKEND:\n${backendResult}\n\nFRONTEND:\n${frontendResult}\n\nFinde alle Bugs, Konventionsverletzungen und fehlende Teile. Gib Fixes.`
);
await runAgent('qa',
`Aufgabe: "${task}"\n\nErstelle konkrete Testschritte, Security-Checks und Edge Cases.`
);
send({ type: 'done' });
res.end();
} catch (e) {
try {
res.write(`data: ${JSON.stringify({ type: 'error', text: e.message })}\n\n`);
res.end();
} catch {}
}
});
return;
}
res.writeHead(404);
res.end();
});
server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('╔═══════════════════════════════════════╗');
console.log('║ IT Nexus Dev Team — Bereit! ║');
console.log('╠═══════════════════════════════════════╣');
console.log(`║ → http://localhost:${PORT}`);
console.log('║ ║');
console.log('║ 6 Agenten: PM, Architect, ║');
console.log('║ Backend, Frontend, Senior, QA ║');
console.log('╚═══════════════════════════════════════╝');
console.log('');
if (!API_KEY) {
console.log('⚠️ Kein ANTHROPIC_API_KEY gesetzt — im Browser eingeben');
console.log(' Oder: set ANTHROPIC_API_KEY=sk-ant-... && node devteam.js');
}
console.log('');
});

View File

@@ -13,9 +13,15 @@ services:
environment:
- NODE_ENV=production
- PORT=5000
- JWT_SECRET=jwt-jItWbLMHTIDElss0C3tMN6NV3hq3GjEIPXp79-X8ljiMS6OMX541Xg
- AGENT_API_KEY=itx-4CPJPTHmCfdrL9D62WacCATEvuvULXcp7ECMpSaNUjsS344F6_L4Ug
- SCANNER_API_KEY=nsx-aa0193d719c2be765441e1dd24bf7480
- JWT_SECRET=${JWT_SECRET}
- AGENT_API_KEY=${AGENT_API_KEY}
- SCANNER_API_KEY=${SCANNER_API_KEY}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- TV_DASHBOARD_KEY=${TV_DASHBOARD_KEY}
# Globaler Fallback für Agents ohne Patch-Gruppe ODER ohne ?hostname-Parameter (alte Agent-Versionen
# vor v2.5.0) — bei jedem neuen Agent-Release IMMER mit hochziehen, sonst fallen alte/gruppenlose
# Agents auf den hartcodierten Code-Default zurück (war zuvor 2.0.0 — ein echter Downgrade-Bug)!
- AGENT_VERSION=${AGENT_VERSION}
- JWT_EXPIRATION=8h
- DATABASE_PATH=/app/data/database.sqlite
- FRONTEND_URL=https://it-nexus.cereda-systems.de
@@ -25,13 +31,13 @@ services:
- SMTP_PORT=587
- SMTP_SECURE=false
- SMTP_USER=it-tool@cereda-systems.de
- SMTP_PASSWORD=Cereda2025!
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_FROM=IT Support Cereda <it-tool@cereda-systems.de>
# IMAP (eingehende E-Mails → Tickets)
- IMAP_HOST=outlook.office365.com
- IMAP_PORT=993
- IMAP_USER=it-tool@cereda-systems.de
- IMAP_PASSWORD=Cereda2025!
- IMAP_PASSWORD=${IMAP_PASSWORD}
- IMAP_MAILBOX=INBOX
# Ticket-Links in E-Mails
- TICKET_BASE_URL=https://it-nexus.cereda-systems.de
@@ -40,21 +46,21 @@ services:
# Microsoft Azure AD (OAuth2 + Graph API)
- AZURE_TENANT_ID=6a9a4382-ea6c-4e64-a09f-0905f83b3d64
- AZURE_CLIENT_ID=de5c4a83-d24d-4323-9f3b-9c3a2f6c709f
- AZURE_CLIENT_SECRET=55f8Q~BAYuFl5-zsX4Aob397PKLZssNTVc9rFb9n
- AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
- AZURE_REDIRECT_URI=https://it-nexus.cereda-systems.de/api/auth/microsoft/callback
# Anthropic Claude API (KI-Integration)
- ANTHROPIC_API_KEY=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
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
# Teams Bot
- TEAMS_BOT_APP_ID=bd39de7e-1b85-4e0c-9301-a68cd5122523
- TEAMS_BOT_APP_SECRET=jos8Q~UjN.Z65.3e9JeXpeKJdX6li58I2EnhfbZQ
- TEAMS_BOT_APP_SECRET=${TEAMS_BOT_APP_SECRET}
# Microsoft Planner Sync
- PLANNER_PLAN_ID=CFyoAifRzUi2ONlk01smWpcAHtm3
# Proxmox Monitoring
- PROXMOX_HOST=https://192.168.0.184:8006
- PROXMOX_TOKEN=root@pam!monitoring=ca287965-3d33-49ec-a069-1bb80fd2e95d
- PROXMOX_TOKEN=${PROXMOX_TOKEN}
- PROXMOX_NODE=hve-01
# GitHub API (Feedback-System)
- GITHUB_TOKEN=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
- GITHUB_TOKEN=${GITHUB_TOKEN}
volumes:
# Persistente Datenbank - bleibt auch nach Container-Neustart erhalten
- fido-data:/app/data

View File

@@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net472</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>DomainJoinTool</AssemblyName>
<RootNamespace>DomainJoinTool</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>icon.ico</ApplicationIcon>
<LangVersion>9.0</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<Optimize>true</Optimize>
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Management.Automation" Condition="Exists('$(PSHOME)\System.Management.Automation.dll')">
<HintPath>$(PSHOME)\System.Management.Automation.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -1,642 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
namespace DomainJoinTool
{
public class MainForm : Form
{
// ── Cereda/Zelo Colors ────────────────────────────────────────────────
static readonly Color CTeal = ColorTranslator.FromHtml("#0D9488");
static readonly Color CTealD = ColorTranslator.FromHtml("#0F766E");
static readonly Color CGreen = ColorTranslator.FromHtml("#059669");
static readonly Color CRed = ColorTranslator.FromHtml("#DC2626");
static readonly Color CAmber = ColorTranslator.FromHtml("#D97706");
static readonly Color CGray = ColorTranslator.FromHtml("#6B7280");
static readonly Color CBG = ColorTranslator.FromHtml("#F8FAFC");
static readonly Color CBorder= ColorTranslator.FromHtml("#E2E8F0");
static readonly Color CText = ColorTranslator.FromHtml("#0F172A");
static readonly Color CText2 = ColorTranslator.FromHtml("#64748B");
// ── State ─────────────────────────────────────────────────────────────
bool _entra, _domain; string _dn = ""; int _step;
Timer _animTimer; float _phase, _spin;
// ── Controls ──────────────────────────────────────────────────────────
StatusBadge _badge;
Panel _body1, _body2;
ModernInput _txUser, _txPass;
RoundButton _btnJoin, _btnLeave, _btnMig;
Label _lnkBack, _lnkTab1, _lnkTab2;
ComboBox _cbSrc, _cbDst;
ProgressBar _pb; Label _lbPb;
RichTextBox _log;
Label _lblStat;
Label _lblMigStatus;
ProgressBar _pbMig;
Label _lblMigCount;
RichTextBox _logMig;
const string DOM = "winkel.local";
const string LOG = @"C:\ProgramData\DomainJoinTool\join.log";
const string FLG = @"C:\ProgramData\DomainJoinTool\pending-migration.txt";
const string EXE = @"C:\ProgramData\DomainJoinTool\DomainJoinTool.exe";
public MainForm()
{
Text = "Cereda Systems · Domain Join Tool";
ClientSize = new Size(620, 680);
FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false; StartPosition = FormStartPosition.CenterScreen;
BackColor = CBG; Font = new Font("Segoe UI", 9f); Icon = MakeIcon();
Build();
_animTimer = new Timer { Interval = 16 };
_animTimer.Tick += (s, e) => { _phase = (_phase + 0.025f) % 1f; _spin = (_spin + 9f) % 360f; _badge?.Tick(_phase, _spin); };
_animTimer.Start();
DetectStatus();
LoadProfiles();
}
void Build()
{
// ── Header ────────────────────────────────────────────────────────
var hdr = new Panel { Dock = DockStyle.Top, Height = 64, BackColor = CTeal };
hdr.Paint += (s, e) =>
{
var g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
// Subtle darker stripe at bottom
using (var br = new SolidBrush(Color.FromArgb(30, 0, 0, 0)))
g.FillRectangle(br, 0, hdr.Height - 3, hdr.Width, 3);
// Logo circle
using (var br = new SolidBrush(Color.FromArgb(40, 255, 255, 255)))
g.FillEllipse(br, 14, 10, 36, 36);
using (var f = new Font("Segoe UI", 11f, FontStyle.Bold))
using (var br = new SolidBrush(Color.White))
{ var sf = new System.Drawing.StringFormat { Alignment = System.Drawing.StringAlignment.Center, LineAlignment = System.Drawing.StringAlignment.Center }; g.DrawString("IT", f, br, new RectangleF(14, 10, 36, 36), sf); }
// Text
using (var br = new SolidBrush(Color.FromArgb(200, 255, 255, 255)))
g.DrawString("CEREDA SYSTEMS", new Font("Segoe UI", 7.5f, FontStyle.Bold), br, 60, 12);
g.DrawString("Domain Join Tool", new Font("Segoe UI", 13f, FontStyle.Bold), Brushes.White, 58, 28);
// Version badge
using (var br = new SolidBrush(Color.FromArgb(40, 255, 255, 255)))
g.FillRectangle(br, hdr.Width - 68, 20, 56, 20);
using (var br = new SolidBrush(Color.FromArgb(200, 255, 255, 255)))
g.DrawString("v2.5.0", new Font("Segoe UI", 8f), br, new RectangleF(hdr.Width - 68, 20, 56, 20),
new System.Drawing.StringFormat { Alignment = System.Drawing.StringAlignment.Center, LineAlignment = System.Drawing.StringAlignment.Center });
};
Controls.Add(hdr);
// ── Tab bar ───────────────────────────────────────────────────────
var tabBar = new Panel { Dock = DockStyle.Top, Height = 38, BackColor = Color.White };
tabBar.Paint += (s, e) => {
e.Graphics.DrawLine(new Pen(CBorder), 0, 37, 600, 37);
// Active indicator
if (_step == 0) e.Graphics.FillRectangle(new SolidBrush(CTeal), 16, 35, 120, 3);
else e.Graphics.FillRectangle(new SolidBrush(CTeal), 148, 35, 140, 3);
};
_lnkTab1 = TabLabel("1 · Domain Join", new Point(16, 8), true);
_lnkTab2 = TabLabel("2 · Profil Migration", new Point(148, 8), false);
_lnkTab1.Click += (s, e) => GoTo(0, tabBar);
_lnkTab2.Click += (s, e) => GoTo(1, tabBar);
tabBar.Controls.Add(_lnkTab1); tabBar.Controls.Add(_lnkTab2);
Controls.Add(tabBar);
// ── Bodies (explicit position to avoid Dock=Fill overlap bug) ────
// hdr=64 + tabBar=38 = 102px
_body1 = new Panel { Location = new Point(0, 102), Size = new Size(620, 578), BackColor = CBG };
_body2 = new Panel { Location = new Point(0, 102), Size = new Size(620, 578), BackColor = CBG, Visible = false };
BuildBody1(); BuildBody2();
Controls.Add(_body1); Controls.Add(_body2);
}
Label TabLabel(string t, Point loc, bool active)
{
var l = new Label { Text = t, Font = new Font("Segoe UI", 9f, active ? FontStyle.Bold : FontStyle.Regular), ForeColor = active ? CTeal : CText2, Location = loc, AutoSize = true, Cursor = Cursors.Hand };
l.MouseEnter += (s, e) => l.ForeColor = CTeal;
l.MouseLeave += (s, e) => l.ForeColor = (_step == (l == _lnkTab1 ? 0 : 1)) ? CTeal : CText2;
return l;
}
void GoTo(int step, Panel tabBar = null)
{
_step = step; _body1.Visible = step == 0; _body2.Visible = step == 1;
_lnkTab1.Font = new Font("Segoe UI", 9f, step == 0 ? FontStyle.Bold : FontStyle.Regular);
_lnkTab2.Font = new Font("Segoe UI", 9f, step == 1 ? FontStyle.Bold : FontStyle.Regular);
_lnkTab1.ForeColor = step == 0 ? CTeal : CText2;
_lnkTab2.ForeColor = step == 1 ? CTeal : CText2;
(tabBar ?? _body1.Parent?.Parent as Panel)?.Invalidate();
// Find and invalidate the tabBar
foreach (Control c in Controls) if (c is Panel p && p.Height == 38) { p.Invalidate(); break; }
}
// ── Body 1: Domain Join ───────────────────────────────────────────────
void BuildBody1()
{
int m = 16, y = 14, w = 588;
// ── Status Card ───────────────────────────────────────────────────
var statCard = MkCard(m, y, w, 86); y += 98;
_lblStat = new Label { Text = "Status", Font = new Font("Segoe UI", 9f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 10), AutoSize = true };
statCard.Controls.Add(_lblStat);
statCard.Controls.Add(new Panel { Location = new Point(20, 30), Size = new Size(w - 26, 1), BackColor = CBorder });
_badge = new StatusBadge { Location = new Point(20, 40) };
statCard.Controls.Add(_badge);
// Right: Entra → AD
statCard.Controls.Add(new Label { Text = "Entra ID", Font = new Font("Segoe UI", 8.5f, FontStyle.Bold), ForeColor = CText2, Location = new Point(280, 38), AutoSize = true });
statCard.Controls.Add(new Label { Text = "→", Font = new Font("Segoe UI", 14f), ForeColor = Color.FromArgb(180, 180, 200), Location = new Point(358, 32), AutoSize = true });
statCard.Controls.Add(new Label { Text = DOM, Font = new Font("Segoe UI", 8.5f, FontStyle.Bold), ForeColor = CGreen, Location = new Point(382, 38), AutoSize = true });
statCard.Controls.Add(new Label { Text = Environment.MachineName, Font = new Font("Cascadia Mono", 7.5f), ForeColor = CText2, Location = new Point(280, 57), AutoSize = true });
_body1.Controls.Add(statCard);
// ── Credentials Card ──────────────────────────────────────────────
var credCard = MkCard(m, y, w, 168); y += 180;
credCard.Controls.Add(new Label { Text = "Domain-Zugangsdaten", Font = new Font("Segoe UI", 10f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 12), AutoSize = true });
credCard.Controls.Add(new Label { Text = "Ziel: " + DOM, Font = new Font("Cascadia Mono", 8f, FontStyle.Bold), ForeColor = CGreen, Location = new Point(220, 15), AutoSize = true });
credCard.Controls.Add(new Panel { Location = new Point(20, 36), Size = new Size(w - 26, 1), BackColor = CBorder });
_txUser = Inp(new Point(20, 44), new Size(w - 40, 52), "Benutzername"); _txUser.Val = "Administrator";
_txPass = Inp(new Point(20, 104), new Size(w - 40, 52), "Passwort", true);
credCard.Controls.Add(_txUser); credCard.Controls.Add(_txPass);
_body1.Controls.Add(credCard);
// ── Action Buttons ────────────────────────────────────────────────
_btnJoin = new RoundButton("▶ Entra leaven + Domain joinen", CTeal, 10) { Location = new Point(m, y), Size = new Size(420, 44) };
_btnLeave = new RoundButton("■ Nur Entra verlassen", CRed, 10) { Location = new Point(m + 428, y), Size = new Size(160, 44) };
_btnJoin.Click += DoJoin; _btnLeave.Click += DoLeave;
_body1.Controls.Add(_btnJoin); _body1.Controls.Add(_btnLeave); y += 52;
// ── Progress ─────────────────────────────────────────────────────
_pb = new ProgressBar { Location = new Point(m, y), Size = new Size(w, 4), Style = ProgressBarStyle.Marquee, MarqueeAnimationSpeed = 25, Visible = false }; y += 8;
_lbPb = Lbl("Bitte warten…", new Point(m, y)); _lbPb.Visible = false; y += 22;
_body1.Controls.Add(_pb); _body1.Controls.Add(_lbPb);
// ── Log ───────────────────────────────────────────────────────────
_body1.Controls.Add(Lbl("Log · " + LOG, new Point(m, y), new Font("Cascadia Mono", 7.5f), CText2)); y += 16;
_log = new RichTextBox { Location = new Point(m, y), Size = new Size(w, 100), BackColor = Color.FromArgb(13, 17, 23), ForeColor = Color.FromArgb(100, 210, 130), Font = new Font("Cascadia Mono", 8f), ReadOnly = true, BorderStyle = BorderStyle.None };
_body1.Controls.Add(_log);
}
// ── Body 2: Migration ─────────────────────────────────────────────────
void BuildBody2()
{
int m = 16, y = 14, w = 588;
var infoCard = MkCard(m, y, w, 140); y += 152;
infoCard.Controls.Add(new Label { Text = "Was wird migriert", Font = new Font("Segoe UI", 10f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 12), AutoSize = true });
infoCard.Controls.Add(new Panel { Location = new Point(20, 36), Size = new Size(w - 26, 1), BackColor = CBorder });
int iy = 46;
foreach (var (t, ok) in new[] {
("Dokumente, Desktop, Downloads, Bilder, Videos, Musik", true),
("AppData\\Roaming — Outlook, Edge, Chrome, App-Einstellungen", true),
("Hintergrundbild, Taskbar-Pins, Startmenü-Layout", true),
("Temp-Cache, NTUSER.DAT — nicht übertragen", false),
}) { infoCard.Controls.Add(Lbl((ok ? "✓ " : "✗ ") + t, new Point(20, iy), null, ok ? CGreen : CAmber)); iy += 24; }
_body2.Controls.Add(infoCard);
var selCard = MkCard(m, y, w, 108); y += 120;
selCard.Controls.Add(new Label { Text = "Profile auswählen", Font = new Font("Segoe UI", 10f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(20, 12), AutoSize = true });
selCard.Controls.Add(new Panel { Location = new Point(20, 36), Size = new Size(w - 26, 1), BackColor = CBorder });
selCard.Controls.Add(Lbl("Von (altes Entra-Profil):", new Point(20, 46)));
selCard.Controls.Add(Lbl("Nach (neues Domain-Profil):", new Point(20, 76)));
_cbSrc = Cmb(new Point(210, 43), new Size(w - 230, 24));
_cbDst = Cmb(new Point(210, 73), new Size(w - 230, 24));
selCard.Controls.Add(_cbSrc); selCard.Controls.Add(_cbDst);
_body2.Controls.Add(selCard);
_btnMig = new RoundButton("▶ Profil jetzt migrieren", CGreen, 10) { Location = new Point(m, y), Size = new Size(440, 44) }; y += 52;
_btnMig.Click += DoMig;
_lnkBack = new Label { Text = "← Zurück zu Domain Join", Font = new Font("Segoe UI", 9f), ForeColor = CText2, Location = new Point(m + 448, y - 40), AutoSize = true, Cursor = Cursors.Hand };
_lnkBack.Click += (s, e) => GoTo(0);
_lnkBack.MouseEnter += (s, e) => _lnkBack.ForeColor = CTeal;
_lnkBack.MouseLeave += (s, e) => _lnkBack.ForeColor = CText2;
_body2.Controls.Add(_btnMig); _body2.Controls.Add(_lnkBack);
// ── Status + Progress + Log (Tab 2) ──────────────────────────────
_lblMigStatus = new Label { Text = "", Font = new Font("Segoe UI", 9f, FontStyle.Bold), ForeColor = CTealD, Location = new Point(m, y), AutoSize = true };
_lblMigCount = new Label { Text = "", Font = new Font("Cascadia Mono", 8f), ForeColor = CText2, Location = new Point(m + 300, y), AutoSize = true };
y += 22;
_pbMig = new ProgressBar { Location = new Point(m, y), Size = new Size(w, 6), Style = ProgressBarStyle.Marquee, MarqueeAnimationSpeed = 20, Visible = false }; y += 14;
_logMig = new RichTextBox { Location = new Point(m, y), Size = new Size(w, 106), BackColor = Color.FromArgb(13, 17, 23), ForeColor = Color.FromArgb(100, 210, 130), Font = new Font("Cascadia Mono", 8f), ReadOnly = true, BorderStyle = BorderStyle.None, Visible = false };
_body2.Controls.Add(_lblMigStatus); _body2.Controls.Add(_lblMigCount);
_body2.Controls.Add(_pbMig); _body2.Controls.Add(_logMig);
}
// ── State ─────────────────────────────────────────────────────────────
void DetectStatus()
{
Task.Run(new Action(() => {
try { var r = Run("dsregcmd", "/status", 10000); _entra = r.Contains("AzureAdJoined : YES"); _domain = r.Contains("DomainJoined : YES"); foreach (var ln in r.Split('\n')) if (ln.Trim().StartsWith("DomainName :")) { _dn = ln.Split(':')[1].Trim(); break; } } catch { }
Invoke(new Action(() => { RefreshStat(); CheckAutoMig(); }));
}));
}
void RefreshStat()
{
if (_domain) { _badge.Mode = StatusBadge.BMode.Joined; _lblStat.Text = "Domain joined: " + _dn; _lblStat.ForeColor = CGreen; _btnJoin.Text = "↺ Domain wechseln"; _btnLeave.Text = "■ Verlassen"; }
else if (_entra) { _badge.Mode = StatusBadge.BMode.Entra; _lblStat.Text = "Entra ID joined — bereit für Domain-Join"; _lblStat.ForeColor = CTealD; }
else { _badge.Mode = StatusBadge.BMode.None; _lblStat.Text = "Workgroup — kein Domain-Join"; _lblStat.ForeColor = CAmber; _btnLeave.Enabled = false; }
}
void CheckAutoMig()
{
if (!File.Exists(FLG) || !_domain) return;
try {
var old = File.ReadAllText(FLG).Trim(); LoadProfiles();
for (int i = 0; i < _cbSrc.Items.Count; i++) if (_cbSrc.Items[i].ToString().Equals(old, StringComparison.OrdinalIgnoreCase)) { _cbSrc.SelectedIndex = i; break; }
var cur = Environment.UserName;
for (int i = 0; i < _cbDst.Items.Count; i++) if (_cbDst.Items[i].ToString().Equals(cur, StringComparison.OrdinalIgnoreCase)) { _cbDst.SelectedIndex = i; break; }
GoTo(1);
if (MessageBox.Show("Domain-Join erfolgreich!\n\nProfil migrieren?\n Von: C:\\Users\\" + old + "\n Nach: C:\\Users\\" + cur, "Profil Migration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
DoMig(null, EventArgs.Empty);
else File.Delete(FLG);
} catch { }
}
void LoadProfiles()
{
if (_cbSrc == null) return;
var list = new List<string>();
if (Directory.Exists(@"C:\Users")) foreach (var d in Directory.GetDirectories(@"C:\Users")) { var n = Path.GetFileName(d); if (n != "Public" && n != "Default" && n != "Default User" && n != "All Users") list.Add(n); }
_cbSrc.Items.Clear(); _cbDst.Items.Clear();
foreach (var p in list) { _cbSrc.Items.Add(p); _cbDst.Items.Add(p); }
if (_cbSrc.Items.Count > 0) _cbSrc.SelectedIndex = 0;
if (_cbDst.Items.Count > 1) _cbDst.SelectedIndex = 1;
}
// ── Actions ───────────────────────────────────────────────────────────
async void DoJoin(object s, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_txUser.Val) || string.IsNullOrWhiteSpace(_txPass.Val)) { MessageBox.Show("Bitte Benutzername und Passwort eingeben.", "Eingabe fehlt", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
if (MessageBox.Show("Computer wird der Domain '" + DOM + "' beigetreten.\n\nFortfahren?", "Bestätigung", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
Busy(true, "Domain-Wechsel läuft…"); _badge.Mode = StatusBadge.BMode.Working;
var user = _txUser.Val.Trim(); var pass = _txPass.Val;
await Task.Run(new Action(async () => {
if (_entra) { Prog("Entra ID verlassen…"); Log(Run("dsregcmd", "/leave", 30000)); await Task.Delay(2000); }
Prog("Domain joinen…");
if (!user.Contains("\\") && !user.Contains("@")) user = DOM + "\\" + user;
var res = PS("$pw=ConvertTo-SecureString '" + Esc(pass) + "' -AsPlainText -Force; $cr=New-Object System.Management.Automation.PSCredential('" + Esc(user) + "',$pw); Add-Computer -DomainName '" + DOM + "' -Credential $cr -Force 2>&1");
Log(res);
var ok = !res.ToLower().Contains("fehler") && !res.ToLower().Contains("error") && !res.ToLower().Contains("failed") && !res.ToLower().Contains("denied");
if (ok) SetupAutoMig();
Invoke(new Action(() => {
Busy(false, "");
if (ok) { if (MessageBox.Show("Erfolgreich!\n\nJetzt neu starten?", "✓ Fertig", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes) Run("shutdown", "/r /t 5 /c \"Domain Join\""); }
else MessageBox.Show("Fehlgeschlagen — Log prüfen.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
DetectStatus();
}));
}));
}
async void DoLeave(object s, EventArgs e)
{
if (MessageBox.Show("Computer verlässt " + (_domain ? "Domain '" + _dn + "'" : "Entra ID") + ".\n\nFortfahren?", "Bestätigung", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
Busy(true, "Verlasse…"); _badge.Mode = StatusBadge.BMode.Working;
await Task.Run(new Action(() => { Log(Run("dsregcmd", "/leave", 30000)); Invoke(new Action(() => { Busy(false, ""); MessageBox.Show("Fertig. Bitte neu starten.", "Erledigt", MessageBoxButtons.OK, MessageBoxIcon.Information); DetectStatus(); })); }));
}
void SetupAutoMig()
{
try { Directory.CreateDirectory(Path.GetDirectoryName(FLG)); File.WriteAllText(FLG, Environment.UserName); File.Copy(System.Reflection.Assembly.GetExecutingAssembly().Location, EXE, true); using (var k = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce", true)) k.SetValue("DomainJoinMigration", "\"" + EXE + "\""); Log("[AUTO] Migration startet nach nächstem Login automatisch"); } catch (Exception ex) { Log("[WARN] " + ex.Message); }
}
async void DoMig(object s, EventArgs e)
{
if (_cbSrc.SelectedItem == null || _cbDst.SelectedItem == null)
{
MessageBox.Show("Bitte Quell- und Zielprofil auswählen.", "Profil fehlt", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var src = _cbSrc.SelectedItem.ToString();
var dst = _cbDst.SelectedItem.ToString();
if (src == dst) { MessageBox.Show("Quelle und Ziel sind identisch.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
var srcPath = @"C:\Users\" + src;
var dstPath = @"C:\Users\" + dst;
if (!Directory.Exists(srcPath)) { MessageBox.Show("Quellprofil nicht gefunden:\n" + srcPath, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); return; }
if (MessageBox.Show("Profil kopieren?\n\nVon: " + srcPath + "\nNach: " + dstPath + "\n\nDas kann mehrere Minuten dauern.", "Bestätigung", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
_btnMig.Enabled = false;
_logMig.Visible = true;
_logMig.Clear();
_pbMig.Visible = true;
_lblMigCount.Text = "";
MigLog("[START] " + src + " → " + dst);
MigStatus("⏳ Migration läuft…");
Log("[START] Profil-Migration: " + src + " → " + dst);
await Task.Run(new Action(() =>
{
try
{
if (!Directory.Exists(dstPath))
{
Directory.CreateDirectory(dstPath);
MigLog("[INFO] Zielordner angelegt: " + dstPath);
}
var args = "\"" + srcPath + "\" \"" + dstPath + "\""
+ " /E /XJ /XC /XN /XO"
+ " /XA:O"
+ " /XD Temp INetCache WebCache \"AppData\\Local\\Temp\" OneDrive \"Cereda Systems GmbH\" \"OneDrive - Cereda Systems GmbH\""
+ " /XF NTUSER.DAT ntuser.dat ntuser.ini \"*.regtrans-ms\" \"*.blf\""
+ " /R:0 /W:0 /NP";
MigLog("[CMD] robocopy " + args);
var psi = new System.Diagnostics.ProcessStartInfo(
@"C:\Windows\System32\robocopy.exe", args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
int exitCode = -1;
int copiedFiles = 0, skippedFiles = 0;
using (var proc = System.Diagnostics.Process.Start(psi))
{
string line;
while ((line = proc.StandardOutput.ReadLine()) != null)
{
MigLog(line);
// Count copied files ("Neue Datei" or "New File")
if (line.Contains("Neue Datei") || line.Contains("New File"))
{
copiedFiles++;
MigCount("Kopiert: " + copiedFiles + " Dateien" + (skippedFiles > 0 ? " • Übersprungen: " + skippedFiles : ""));
}
else if (line.Contains("FEHLER") || line.Contains("ERROR"))
{
skippedFiles++;
MigCount("Kopiert: " + copiedFiles + " Dateien • Übersprungen: " + skippedFiles);
}
}
proc.WaitForExit(1800000);
exitCode = proc.ExitCode;
}
Invoke(new Action(() => _pbMig.Visible = false));
// robocopy: 0-7 = OK (0=nichts zu tun, 1=kopiert, 2=extra, etc.), 8+ = Fehler
if (exitCode >= 8)
{
MigLog("[FEHLER] ExitCode=" + exitCode);
MigStatus("✗ Fehler (ExitCode " + exitCode + ")");
}
else
{
MigLog("[OK] Abgeschlossen. ExitCode=" + exitCode);
MigStatus("✓ Migration abgeschlossen — " + copiedFiles + " Dateien kopiert");
}
try { if (File.Exists(FLG)) File.Delete(FLG); } catch { }
Invoke(new Action(() =>
{
_btnMig.Enabled = true;
MessageBox.Show("Migration abgeschlossen!\n\nDateien sind in:\n" + dstPath,
"✓ Fertig", MessageBoxButtons.OK, MessageBoxIcon.Information);
}));
}
catch (Exception ex)
{
MigLog("[EXCEPTION] " + ex.Message);
MigStatus("✗ Fehler aufgetreten");
Log("[EXCEPTION] Migration: " + ex.Message);
Invoke(new Action(() =>
{
_btnMig.Enabled = true;
MessageBox.Show("Fehler bei Migration:\n\n" + ex.Message, "Fehler",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}));
}
}));
}
// ── Helpers ───────────────────────────────────────────────────────────
void Busy(bool b, string m) { if (InvokeRequired) { Invoke(new Action(() => Busy(b, m))); return; } _btnJoin.Enabled = _btnLeave.Enabled = !b; _pb.Visible = _lbPb.Visible = b; if (!string.IsNullOrEmpty(m)) _lbPb.Text = m; }
void Prog(string m) { if (InvokeRequired) { Invoke(new Action(() => Prog(m))); return; } _lbPb.Text = m; Log(m); }
void Log(string t) { if (InvokeRequired) { Invoke(new Action(() => Log(t))); return; } if (string.IsNullOrWhiteSpace(t)) return; var ln = "[" + DateTime.Now.ToString("HH:mm:ss") + "] " + t.Trim(); _log?.AppendText(ln + "\n"); _log?.ScrollToCaret(); try { Directory.CreateDirectory(Path.GetDirectoryName(LOG)); File.AppendAllText(LOG, DateTime.Now.ToString("yyyy-MM-dd") + " " + ln + Environment.NewLine); } catch { } }
void MigLog(string t) { if (InvokeRequired) { Invoke(new Action(() => MigLog(t))); return; } if (string.IsNullOrWhiteSpace(t)) return; _logMig?.AppendText(t.TrimEnd() + "\n"); _logMig?.ScrollToCaret(); }
void MigStatus(string t) { if (InvokeRequired) { Invoke(new Action(() => MigStatus(t))); return; } if (_lblMigStatus != null) { _lblMigStatus.Text = t; _lblMigStatus.ForeColor = t.StartsWith("✓") ? CGreen : t.StartsWith("✗") ? CRed : CTealD; } }
void MigCount(string t) { if (InvokeRequired) { Invoke(new Action(() => MigCount(t))); return; } if (_lblMigCount != null) _lblMigCount.Text = t; }
// Same pattern as ZeloServerVerwaltung
Panel MkCard(int x, int y, int w, int h)
{
var p = new Panel { Location = new Point(x, y), Size = new Size(w, h), BackColor = Color.White };
var accent = new Panel { Location = Point.Empty, Size = new Size(5, h), BackColor = CTeal };
p.Controls.Add(accent);
p.Paint += (s, e) =>
{
var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias;
// Border
using (var path = RR(1, 1, w - 2, h - 2, 10))
using (var pen = new Pen(Color.FromArgb(215, 225, 235), 1f)) g.DrawPath(pen, path);
// Subtle bottom shadow line
using (var pen = new Pen(Color.FromArgb(50, 0, 100, 150), 1.5f))
g.DrawLine(pen, 10, h - 1, w - 4, h - 1);
};
using (var path = RR(0, 0, w, h, 10))
p.Region = new Region(path);
return p;
}
Label Lbl(string t, Point loc, Font f = null, Color? c = null) => new Label { Text = t, Font = f ?? new Font("Segoe UI", 9f), ForeColor = c ?? CText2, Location = loc, AutoSize = true };
ModernInput Inp(Point loc, Size sz, string placeholder = "", bool pw = false)
{
return new ModernInput(placeholder, pw) { Location = loc, Size = sz };
}
ComboBox Cmb(Point loc, Size sz) => new ComboBox { Location = loc, Size = sz, DropDownStyle = ComboBoxStyle.DropDownList, FlatStyle = FlatStyle.Flat, Font = new Font("Segoe UI", 9f) };
static GraphicsPath RR(int x, int y, int w, int h, int r) { var p = new GraphicsPath(); p.AddArc(x, y, r * 2, r * 2, 180, 90); p.AddArc(x + w - r * 2, y, r * 2, r * 2, 270, 90); p.AddArc(x + w - r * 2, y + h - r * 2, r * 2, r * 2, 0, 90); p.AddArc(x, y + h - r * 2, r * 2, r * 2, 90, 90); p.CloseFigure(); return p; }
static Icon MakeIcon()
{
var bmp = new Bitmap(32, 32);
using (var g = Graphics.FromImage(bmp))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
// Teal rounded square
using (var path = RR(0, 0, 32, 32, 6))
using (var br = new SolidBrush(ColorTranslator.FromHtml("#0D9488")))
g.FillPath(br, path);
// White "C" letter
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString("C", new Font("Segoe UI", 16f, FontStyle.Bold), Brushes.White, new RectangleF(0, 0, 32, 32), sf);
}
IntPtr hIcon = bmp.GetHicon();
return Icon.FromHandle(hIcon);
}
string Run(string exe, string args, int ms = 60000) { var psi = new ProcessStartInfo(exe, args) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using (var p = Process.Start(psi)) { var o = p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd(); p.WaitForExit(ms); return o; } }
string PS(string sc) => Run("powershell.exe", "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"" + sc.Replace("\"", "\\\"") + "\"", 120000);
static string Esc(string s) => s.Replace("'", "''");
protected override void OnFormClosed(FormClosedEventArgs e) { _animTimer?.Stop(); _animTimer?.Dispose(); base.OnFormClosed(e); }
// ── ModernInput — Floating-Label + Fokus-Effekt + Validierung ─────────
class ModernInput : Panel
{
readonly Label _lbl;
readonly TextBox _txt;
readonly Label _icon;
bool _focused, _ready;
static readonly Color Teal = ColorTranslator.FromHtml("#0D9488");
static readonly Color Gray = ColorTranslator.FromHtml("#94A3B8");
static readonly Color Border = ColorTranslator.FromHtml("#E2E8F0");
static readonly Color Green = ColorTranslator.FromHtml("#059669");
static readonly Color Red = ColorTranslator.FromHtml("#DC2626");
public string Val { get => _txt.Text; set { _txt.Text = value; Update_(); } }
public ModernInput(string placeholder, bool pw = false)
{
DoubleBuffered = true;
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);
BackColor = Color.Transparent; Height = 52;
_lbl = new Label {
Text = placeholder,
Font = new Font("Segoe UI", 9.5f),
ForeColor = Gray,
Location = new Point(13, 15),
AutoSize = true
};
_txt = new TextBox {
BorderStyle = BorderStyle.None,
Location = new Point(13, 28),
Font = new Font("Segoe UI", 10.5f),
BackColor = Color.White
};
if (pw) _txt.PasswordChar = '●';
// Validation icon
_icon = new Label {
Font = new Font("Segoe UI", 12f),
Text = "",
AutoSize = true,
ForeColor = Gray
};
_txt.GotFocus += (s, e) => { _focused = true; Update_(); Invalidate(); };
_txt.LostFocus += (s, e) => { _focused = false; Update_(); Invalidate(); };
_txt.TextChanged += (s, e) => { Update_(); Invalidate(); };
Controls.Add(_lbl);
Controls.Add(_txt);
Controls.Add(_icon);
_ready = true;
}
void Update_()
{
bool fl = _focused || !string.IsNullOrEmpty(_txt.Text);
_lbl.Font = new Font("Segoe UI", fl ? 7.5f : 9.5f);
_lbl.Location = new Point(13, fl ? 6 : 15);
_lbl.ForeColor = _focused ? Teal : Gray;
bool filled = !string.IsNullOrWhiteSpace(_txt.Text);
_icon.Text = filled ? "✓" : "";
_icon.ForeColor = filled ? Green : Gray;
if (_icon.Width > 0)
_icon.Location = new Point(Width - _icon.Width - 12, (Height - _icon.Height) / 2);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
if (!_ready || _txt == null) return;
_txt.Width = Width - 32;
Update_();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
// White background
using (var br = new SolidBrush(Color.White))
g.FillRectangle(br, 0, 0, Width, Height);
// Border
var bc = _focused ? Teal : Border;
using (var pen = new Pen(bc, _focused ? 2f : 1f))
g.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
// Bottom accent line on focus
if (_focused)
using (var pen = new Pen(Teal, 2.5f))
g.DrawLine(pen, 2, Height - 1, Width - 3, Height - 1);
}
}
// ── RoundButton (same as ZeloServerVerwaltung) ────────────────────────
class RoundButton : Control
{
bool _hov, _press; readonly Color _base, _hov2, _press2; readonly int _r;
public RoundButton(string text, Color baseColor, int radius = 8)
{
_base = baseColor; _hov2 = ControlPaint.Dark(baseColor, 0.10f); _press2 = ControlPaint.Dark(baseColor, 0.20f); _r = radius;
Text = text; Font = new Font("Segoe UI", 9f, FontStyle.Bold); Cursor = Cursors.Hand;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Parent?.BackColor ?? Color.White);
var fill = _press ? _press2 : (_hov && Enabled) ? _hov2 : _base;
using (var path = RR(0, 0, Width, Height, _r)) using (var br = new SolidBrush(fill)) g.FillPath(br, path);
if (!Enabled) using (var path = RR(0, 0, Width, Height, _r)) using (var br = new SolidBrush(Color.FromArgb(110, 255, 255, 255))) g.FillPath(br, path);
TextRenderer.DrawText(e.Graphics, Text, Font, new Rectangle(0, 0, Width, Height), Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}
protected override void OnMouseEnter(EventArgs e) { _hov = true; Invalidate(); base.OnMouseEnter(e); }
protected override void OnMouseLeave(EventArgs e) { _hov = false; _press = false; Invalidate(); base.OnMouseLeave(e); }
protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { _press = true; Invalidate(); } base.OnMouseDown(e); }
protected override void OnMouseUp(MouseEventArgs e) { _press = false; Invalidate(); base.OnMouseUp(e); }
protected override void OnEnabledChanged(EventArgs e) { Cursor = Enabled ? Cursors.Hand : Cursors.Default; Invalidate(); base.OnEnabledChanged(e); }
}
// ── StatusBadge (adapted from ZeloServerVerwaltung) ───────────────────
class StatusBadge : Control
{
public enum BMode { Entra, Joined, None, Working }
BMode _mode; float _phase, _spin;
public BMode Mode { get => _mode; set { _mode = value; Invalidate(); } }
public StatusBadge() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); Size = new Size(230, 28); BackColor = Color.White; }
public void Tick(float ph, float sp) { _phase = ph; _spin = sp; if (_mode != BMode.None) Invalidate(); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; g.Clear(Color.White);
Color bg; string txt;
switch (_mode) {
case BMode.Joined: bg = ColorTranslator.FromHtml("#059669"); txt = "DOMAIN JOINED"; break;
case BMode.Entra: bg = ColorTranslator.FromHtml("#0D9488"); txt = "ENTRA ID JOINED"; break;
case BMode.Working: bg = ColorTranslator.FromHtml("#D97706"); txt = "BITTE WARTEN..."; break;
default: bg = ColorTranslator.FromHtml("#6B7280"); txt = "WORKGROUP"; break;
}
int r = Height / 2;
using (var path = new GraphicsPath()) { path.AddArc(0, 0, r * 2, r * 2, 90, 180); path.AddArc(Width - r * 2, 0, r * 2, r * 2, 270, 180); path.CloseFigure(); using (var br = new SolidBrush(bg)) g.FillPath(br, path); }
// Dot
int dx = r, dy = r, dr = 5;
if (_mode == BMode.Working) { using (var p2 = new Pen(Color.FromArgb(60, Color.White), 2f)) g.DrawEllipse(p2, dx - dr, dy - dr, dr * 2, dr * 2); using (var p2 = new Pen(Color.White, 2f) { StartCap = LineCap.Round, EndCap = LineCap.Round }) g.DrawArc(p2, dx - dr, dy - dr, dr * 2, dr * 2, _spin, 100f); }
else { double pv = Math.Sin(_phase * Math.PI * 2) * 0.5 + 0.5; int gr = dr + (int)(pv * 2); using (var b = new SolidBrush(Color.FromArgb(60, Color.White))) g.FillEllipse(b, dx - gr, dy - gr, gr * 2, gr * 2); using (var b = new SolidBrush(Color.White)) g.FillEllipse(b, dx - dr, dy - dr, dr * 2, dr * 2); }
// Text
var rect = new RectangleF(r * 2 + 6, 0, Width - r * 2 - 6 - r, Height);
var sf = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center };
using (var f = new Font("Segoe UI", 8.5f, FontStyle.Bold)) using (var br = new SolidBrush(Color.White)) g.DrawString(txt, f, br, rect, sf);
}
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Windows.Forms;
namespace DomainJoinTool
{
internal static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
Application.ThreadException += (s, e) =>
System.IO.File.AppendAllText(@"C:\ProgramData\DomainJoinTool\crash.log",
DateTime.Now + ": " + e.Exception + Environment.NewLine);
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
System.IO.File.AppendAllText(@"C:\ProgramData\DomainJoinTool\crash.log",
DateTime.Now + ": " + e.ExceptionObject + Environment.NewLine);
try
{
Application.Run(new MainForm());
}
catch (Exception ex)
{
MessageBox.Show("Startfehler: " + ex.Message + "\n\n" + ex.StackTrace, "Fehler");
}
}
}
}

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="DomainJoinTool.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- Braucht Admin-Rechte für dsregcmd und Add-Computer -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,63 +0,0 @@
{
"format": 1,
"restore": {
"C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj": {}
},
"projects": {
"C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
"projectName": "DomainJoinTool",
"projectPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
"packagesPath": "C:\\Users\\gruessing\\.nuget\\packages\\",
"outputPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\gruessing\\AppData\\Roaming\\NuGet\\NuGet.Config"
],
"originalTargetFrameworks": [
"net472"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net472": {
"targetAlias": "net472",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
}
},
"frameworks": {
"net472": {
"targetAlias": "net472",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"suppressParent": "All",
"target": "Package",
"version": "[1.0.3, )",
"autoReferenced": true
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.421\\RuntimeIdentifierGraph.json"
}
},
"runtimes": {
"win7-x64": {
"#import": []
}
}
}
}
}

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\gruessing\.nuget\packages\</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.11.2</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\gruessing\.nuget\packages\" />
</ItemGroup>
</Project>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net472\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net472.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net472\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net472.targets')" />
</ImportGroup>
</Project>

View File

@@ -1,4 +0,0 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]

View File

@@ -1,22 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("DomainJoinTool")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("DomainJoinTool")]
[assembly: System.Reflection.AssemblyTitleAttribute("DomainJoinTool")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Von der MSBuild WriteCodeFragment-Klasse generiert.

View File

@@ -1 +0,0 @@
d0c739f07a28fc8609fc4a098983a9d7bd13f38d051a4df2bc76519c63de7a08

View File

@@ -1,11 +0,0 @@
is_global = true
build_property.ApplicationManifest = app.manifest
build_property.StartupObject =
build_property.ApplicationDefaultFont =
build_property.ApplicationHighDpiMode =
build_property.ApplicationUseCompatibleTextRendering =
build_property.ApplicationVisualStyles =
build_property.RootNamespace = DomainJoinTool
build_property.ProjectDir = C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =

View File

@@ -1 +0,0 @@
074cd1570727f0b97007ce35e18f8b37cb149bef28991a72fe6b298b163c25a9

View File

@@ -1,8 +0,0 @@
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.csproj.AssemblyReference.cache
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.GeneratedMSBuildEditorConfig.editorconfig
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.AssemblyInfoInputs.cache
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.AssemblyInfo.cs
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.csproj.CoreCompileInputs.cache
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\bin\Release\net472\DomainJoinTool.exe.config
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\bin\Release\net472\DomainJoinTool.exe
C:\Users\gruessing\OneDrive - Cereda Systems GmbH\Desktop\IT Tool\domain-join-tool\obj\Release\net472\DomainJoinTool.exe

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

View File

@@ -1,489 +0,0 @@
{
"version": 3,
"targets": {
".NETFramework,Version=v4.7.2": {
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
"type": "package",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3"
}
},
"Microsoft.NETFramework.ReferenceAssemblies.net472/1.0.3": {
"type": "package",
"build": {
"build/Microsoft.NETFramework.ReferenceAssemblies.net472.targets": {}
}
}
},
".NETFramework,Version=v4.7.2/win7-x64": {
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
"type": "package",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3"
}
},
"Microsoft.NETFramework.ReferenceAssemblies.net472/1.0.3": {
"type": "package",
"build": {
"build/Microsoft.NETFramework.ReferenceAssemblies.net472.targets": {}
}
}
}
},
"libraries": {
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
"sha512": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"type": "package",
"path": "microsoft.netframework.referenceassemblies/1.0.3",
"files": [
".nupkg.metadata",
".signature.p7s",
"microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
"microsoft.netframework.referenceassemblies.nuspec"
]
},
"Microsoft.NETFramework.ReferenceAssemblies.net472/1.0.3": {
"sha512": "0E7evZXHXaDYYiLRfpyXvCh+yzM2rNTyuZDI+ZO7UUqSc6GfjePiXTdqJGtgIKUwdI81tzQKmaWprnUiPj9hAw==",
"type": "package",
"path": "microsoft.netframework.referenceassemblies.net472/1.0.3",
"files": [
".nupkg.metadata",
".signature.p7s",
"build/.NETFramework/v4.7.2/Accessibility.dll",
"build/.NETFramework/v4.7.2/Accessibility.xml",
"build/.NETFramework/v4.7.2/CustomMarshalers.dll",
"build/.NETFramework/v4.7.2/CustomMarshalers.xml",
"build/.NETFramework/v4.7.2/Facades/Microsoft.Win32.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.AppContext.dll",
"build/.NETFramework/v4.7.2/Facades/System.Collections.Concurrent.dll",
"build/.NETFramework/v4.7.2/Facades/System.Collections.NonGeneric.dll",
"build/.NETFramework/v4.7.2/Facades/System.Collections.Specialized.dll",
"build/.NETFramework/v4.7.2/Facades/System.Collections.dll",
"build/.NETFramework/v4.7.2/Facades/System.ComponentModel.Annotations.dll",
"build/.NETFramework/v4.7.2/Facades/System.ComponentModel.EventBasedAsync.dll",
"build/.NETFramework/v4.7.2/Facades/System.ComponentModel.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.ComponentModel.TypeConverter.dll",
"build/.NETFramework/v4.7.2/Facades/System.ComponentModel.dll",
"build/.NETFramework/v4.7.2/Facades/System.Console.dll",
"build/.NETFramework/v4.7.2/Facades/System.Data.Common.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Contracts.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Debug.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.FileVersionInfo.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Process.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.StackTrace.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.TextWriterTraceListener.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.Tools.dll",
"build/.NETFramework/v4.7.2/Facades/System.Diagnostics.TraceSource.dll",
"build/.NETFramework/v4.7.2/Facades/System.Drawing.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.Dynamic.Runtime.dll",
"build/.NETFramework/v4.7.2/Facades/System.Globalization.Calendars.dll",
"build/.NETFramework/v4.7.2/Facades/System.Globalization.Extensions.dll",
"build/.NETFramework/v4.7.2/Facades/System.Globalization.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.Compression.ZipFile.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.DriveInfo.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.Watcher.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.FileSystem.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.IsolatedStorage.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.MemoryMappedFiles.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.Pipes.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.UnmanagedMemoryStream.dll",
"build/.NETFramework/v4.7.2/Facades/System.IO.dll",
"build/.NETFramework/v4.7.2/Facades/System.Linq.Expressions.dll",
"build/.NETFramework/v4.7.2/Facades/System.Linq.Parallel.dll",
"build/.NETFramework/v4.7.2/Facades/System.Linq.Queryable.dll",
"build/.NETFramework/v4.7.2/Facades/System.Linq.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.Http.Rtc.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.NameResolution.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.NetworkInformation.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.Ping.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.Requests.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.Security.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.Sockets.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.WebHeaderCollection.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.WebSockets.Client.dll",
"build/.NETFramework/v4.7.2/Facades/System.Net.WebSockets.dll",
"build/.NETFramework/v4.7.2/Facades/System.ObjectModel.dll",
"build/.NETFramework/v4.7.2/Facades/System.Reflection.Emit.ILGeneration.dll",
"build/.NETFramework/v4.7.2/Facades/System.Reflection.Emit.Lightweight.dll",
"build/.NETFramework/v4.7.2/Facades/System.Reflection.Emit.dll",
"build/.NETFramework/v4.7.2/Facades/System.Reflection.Extensions.dll",
"build/.NETFramework/v4.7.2/Facades/System.Reflection.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.Reflection.dll",
"build/.NETFramework/v4.7.2/Facades/System.Resources.Reader.dll",
"build/.NETFramework/v4.7.2/Facades/System.Resources.ResourceManager.dll",
"build/.NETFramework/v4.7.2/Facades/System.Resources.Writer.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.CompilerServices.VisualC.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.Extensions.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.Handles.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.InteropServices.RuntimeInformation.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.InteropServices.WindowsRuntime.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.InteropServices.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.Numerics.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Formatters.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Json.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.Serialization.Xml.dll",
"build/.NETFramework/v4.7.2/Facades/System.Runtime.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.Claims.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Algorithms.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Csp.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Encoding.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.Cryptography.X509Certificates.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.Principal.dll",
"build/.NETFramework/v4.7.2/Facades/System.Security.SecureString.dll",
"build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Duplex.dll",
"build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Http.dll",
"build/.NETFramework/v4.7.2/Facades/System.ServiceModel.NetTcp.dll",
"build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Primitives.dll",
"build/.NETFramework/v4.7.2/Facades/System.ServiceModel.Security.dll",
"build/.NETFramework/v4.7.2/Facades/System.Text.Encoding.Extensions.dll",
"build/.NETFramework/v4.7.2/Facades/System.Text.Encoding.dll",
"build/.NETFramework/v4.7.2/Facades/System.Text.RegularExpressions.dll",
"build/.NETFramework/v4.7.2/Facades/System.Threading.Overlapped.dll",
"build/.NETFramework/v4.7.2/Facades/System.Threading.Tasks.Parallel.dll",
"build/.NETFramework/v4.7.2/Facades/System.Threading.Tasks.dll",
"build/.NETFramework/v4.7.2/Facades/System.Threading.Thread.dll",
"build/.NETFramework/v4.7.2/Facades/System.Threading.ThreadPool.dll",
"build/.NETFramework/v4.7.2/Facades/System.Threading.Timer.dll",
"build/.NETFramework/v4.7.2/Facades/System.Threading.dll",
"build/.NETFramework/v4.7.2/Facades/System.ValueTuple.dll",
"build/.NETFramework/v4.7.2/Facades/System.Xml.ReaderWriter.dll",
"build/.NETFramework/v4.7.2/Facades/System.Xml.XDocument.dll",
"build/.NETFramework/v4.7.2/Facades/System.Xml.XPath.XDocument.dll",
"build/.NETFramework/v4.7.2/Facades/System.Xml.XPath.dll",
"build/.NETFramework/v4.7.2/Facades/System.Xml.XmlDocument.dll",
"build/.NETFramework/v4.7.2/Facades/System.Xml.XmlSerializer.dll",
"build/.NETFramework/v4.7.2/Facades/netstandard.dll",
"build/.NETFramework/v4.7.2/ISymWrapper.dll",
"build/.NETFramework/v4.7.2/ISymWrapper.xml",
"build/.NETFramework/v4.7.2/Microsoft.Activities.Build.dll",
"build/.NETFramework/v4.7.2/Microsoft.Activities.Build.xml",
"build/.NETFramework/v4.7.2/Microsoft.Build.Conversion.v4.0.dll",
"build/.NETFramework/v4.7.2/Microsoft.Build.Conversion.v4.0.xml",
"build/.NETFramework/v4.7.2/Microsoft.Build.Engine.dll",
"build/.NETFramework/v4.7.2/Microsoft.Build.Engine.xml",
"build/.NETFramework/v4.7.2/Microsoft.Build.Framework.dll",
"build/.NETFramework/v4.7.2/Microsoft.Build.Framework.xml",
"build/.NETFramework/v4.7.2/Microsoft.Build.Tasks.v4.0.dll",
"build/.NETFramework/v4.7.2/Microsoft.Build.Tasks.v4.0.xml",
"build/.NETFramework/v4.7.2/Microsoft.Build.Utilities.v4.0.dll",
"build/.NETFramework/v4.7.2/Microsoft.Build.Utilities.v4.0.xml",
"build/.NETFramework/v4.7.2/Microsoft.Build.dll",
"build/.NETFramework/v4.7.2/Microsoft.Build.xml",
"build/.NETFramework/v4.7.2/Microsoft.CSharp.dll",
"build/.NETFramework/v4.7.2/Microsoft.CSharp.xml",
"build/.NETFramework/v4.7.2/Microsoft.JScript.dll",
"build/.NETFramework/v4.7.2/Microsoft.JScript.xml",
"build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.Data.dll",
"build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.Data.xml",
"build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.dll",
"build/.NETFramework/v4.7.2/Microsoft.VisualBasic.Compatibility.xml",
"build/.NETFramework/v4.7.2/Microsoft.VisualBasic.dll",
"build/.NETFramework/v4.7.2/Microsoft.VisualBasic.xml",
"build/.NETFramework/v4.7.2/Microsoft.VisualC.STLCLR.dll",
"build/.NETFramework/v4.7.2/Microsoft.VisualC.STLCLR.xml",
"build/.NETFramework/v4.7.2/Microsoft.VisualC.dll",
"build/.NETFramework/v4.7.2/Microsoft.VisualC.xml",
"build/.NETFramework/v4.7.2/PermissionSets/FullTrust.xml",
"build/.NETFramework/v4.7.2/PermissionSets/Internet.xml",
"build/.NETFramework/v4.7.2/PermissionSets/LocalIntranet.xml",
"build/.NETFramework/v4.7.2/PresentationBuildTasks.dll",
"build/.NETFramework/v4.7.2/PresentationBuildTasks.xml",
"build/.NETFramework/v4.7.2/PresentationCore.dll",
"build/.NETFramework/v4.7.2/PresentationCore.xml",
"build/.NETFramework/v4.7.2/PresentationFramework.Aero.dll",
"build/.NETFramework/v4.7.2/PresentationFramework.Aero.xml",
"build/.NETFramework/v4.7.2/PresentationFramework.Aero2.dll",
"build/.NETFramework/v4.7.2/PresentationFramework.Aero2.xml",
"build/.NETFramework/v4.7.2/PresentationFramework.AeroLite.dll",
"build/.NETFramework/v4.7.2/PresentationFramework.AeroLite.xml",
"build/.NETFramework/v4.7.2/PresentationFramework.Classic.dll",
"build/.NETFramework/v4.7.2/PresentationFramework.Classic.xml",
"build/.NETFramework/v4.7.2/PresentationFramework.Luna.dll",
"build/.NETFramework/v4.7.2/PresentationFramework.Luna.xml",
"build/.NETFramework/v4.7.2/PresentationFramework.Royale.dll",
"build/.NETFramework/v4.7.2/PresentationFramework.Royale.xml",
"build/.NETFramework/v4.7.2/PresentationFramework.dll",
"build/.NETFramework/v4.7.2/PresentationFramework.xml",
"build/.NETFramework/v4.7.2/ReachFramework.dll",
"build/.NETFramework/v4.7.2/ReachFramework.xml",
"build/.NETFramework/v4.7.2/RedistList/FrameworkList.xml",
"build/.NETFramework/v4.7.2/System.Activities.Core.Presentation.dll",
"build/.NETFramework/v4.7.2/System.Activities.Core.Presentation.xml",
"build/.NETFramework/v4.7.2/System.Activities.DurableInstancing.dll",
"build/.NETFramework/v4.7.2/System.Activities.DurableInstancing.xml",
"build/.NETFramework/v4.7.2/System.Activities.Presentation.dll",
"build/.NETFramework/v4.7.2/System.Activities.Presentation.xml",
"build/.NETFramework/v4.7.2/System.Activities.dll",
"build/.NETFramework/v4.7.2/System.Activities.xml",
"build/.NETFramework/v4.7.2/System.AddIn.Contract.dll",
"build/.NETFramework/v4.7.2/System.AddIn.Contract.xml",
"build/.NETFramework/v4.7.2/System.AddIn.dll",
"build/.NETFramework/v4.7.2/System.AddIn.xml",
"build/.NETFramework/v4.7.2/System.ComponentModel.Composition.Registration.dll",
"build/.NETFramework/v4.7.2/System.ComponentModel.Composition.Registration.xml",
"build/.NETFramework/v4.7.2/System.ComponentModel.Composition.dll",
"build/.NETFramework/v4.7.2/System.ComponentModel.Composition.xml",
"build/.NETFramework/v4.7.2/System.ComponentModel.DataAnnotations.dll",
"build/.NETFramework/v4.7.2/System.ComponentModel.DataAnnotations.xml",
"build/.NETFramework/v4.7.2/System.Configuration.Install.dll",
"build/.NETFramework/v4.7.2/System.Configuration.Install.xml",
"build/.NETFramework/v4.7.2/System.Configuration.dll",
"build/.NETFramework/v4.7.2/System.Configuration.xml",
"build/.NETFramework/v4.7.2/System.Core.dll",
"build/.NETFramework/v4.7.2/System.Core.xml",
"build/.NETFramework/v4.7.2/System.Data.DataSetExtensions.dll",
"build/.NETFramework/v4.7.2/System.Data.DataSetExtensions.xml",
"build/.NETFramework/v4.7.2/System.Data.Entity.Design.dll",
"build/.NETFramework/v4.7.2/System.Data.Entity.Design.xml",
"build/.NETFramework/v4.7.2/System.Data.Entity.dll",
"build/.NETFramework/v4.7.2/System.Data.Entity.xml",
"build/.NETFramework/v4.7.2/System.Data.Linq.dll",
"build/.NETFramework/v4.7.2/System.Data.Linq.xml",
"build/.NETFramework/v4.7.2/System.Data.OracleClient.dll",
"build/.NETFramework/v4.7.2/System.Data.OracleClient.xml",
"build/.NETFramework/v4.7.2/System.Data.Services.Client.dll",
"build/.NETFramework/v4.7.2/System.Data.Services.Client.xml",
"build/.NETFramework/v4.7.2/System.Data.Services.Design.dll",
"build/.NETFramework/v4.7.2/System.Data.Services.Design.xml",
"build/.NETFramework/v4.7.2/System.Data.Services.dll",
"build/.NETFramework/v4.7.2/System.Data.Services.xml",
"build/.NETFramework/v4.7.2/System.Data.SqlXml.dll",
"build/.NETFramework/v4.7.2/System.Data.SqlXml.xml",
"build/.NETFramework/v4.7.2/System.Data.dll",
"build/.NETFramework/v4.7.2/System.Data.xml",
"build/.NETFramework/v4.7.2/System.Deployment.dll",
"build/.NETFramework/v4.7.2/System.Deployment.xml",
"build/.NETFramework/v4.7.2/System.Design.dll",
"build/.NETFramework/v4.7.2/System.Design.xml",
"build/.NETFramework/v4.7.2/System.Device.dll",
"build/.NETFramework/v4.7.2/System.Device.xml",
"build/.NETFramework/v4.7.2/System.Diagnostics.Tracing.dll",
"build/.NETFramework/v4.7.2/System.Diagnostics.Tracing.xml",
"build/.NETFramework/v4.7.2/System.DirectoryServices.AccountManagement.dll",
"build/.NETFramework/v4.7.2/System.DirectoryServices.AccountManagement.xml",
"build/.NETFramework/v4.7.2/System.DirectoryServices.Protocols.dll",
"build/.NETFramework/v4.7.2/System.DirectoryServices.Protocols.xml",
"build/.NETFramework/v4.7.2/System.DirectoryServices.dll",
"build/.NETFramework/v4.7.2/System.DirectoryServices.xml",
"build/.NETFramework/v4.7.2/System.Drawing.Design.dll",
"build/.NETFramework/v4.7.2/System.Drawing.Design.xml",
"build/.NETFramework/v4.7.2/System.Drawing.dll",
"build/.NETFramework/v4.7.2/System.Drawing.xml",
"build/.NETFramework/v4.7.2/System.Dynamic.dll",
"build/.NETFramework/v4.7.2/System.EnterpriseServices.Thunk.dll",
"build/.NETFramework/v4.7.2/System.EnterpriseServices.Wrapper.dll",
"build/.NETFramework/v4.7.2/System.EnterpriseServices.dll",
"build/.NETFramework/v4.7.2/System.EnterpriseServices.xml",
"build/.NETFramework/v4.7.2/System.IO.Compression.FileSystem.dll",
"build/.NETFramework/v4.7.2/System.IO.Compression.FileSystem.xml",
"build/.NETFramework/v4.7.2/System.IO.Compression.dll",
"build/.NETFramework/v4.7.2/System.IO.Compression.xml",
"build/.NETFramework/v4.7.2/System.IO.Log.dll",
"build/.NETFramework/v4.7.2/System.IO.Log.xml",
"build/.NETFramework/v4.7.2/System.IdentityModel.Selectors.dll",
"build/.NETFramework/v4.7.2/System.IdentityModel.Selectors.xml",
"build/.NETFramework/v4.7.2/System.IdentityModel.Services.dll",
"build/.NETFramework/v4.7.2/System.IdentityModel.Services.xml",
"build/.NETFramework/v4.7.2/System.IdentityModel.dll",
"build/.NETFramework/v4.7.2/System.IdentityModel.xml",
"build/.NETFramework/v4.7.2/System.Linq.xml",
"build/.NETFramework/v4.7.2/System.Management.Instrumentation.dll",
"build/.NETFramework/v4.7.2/System.Management.Instrumentation.xml",
"build/.NETFramework/v4.7.2/System.Management.dll",
"build/.NETFramework/v4.7.2/System.Management.xml",
"build/.NETFramework/v4.7.2/System.Messaging.dll",
"build/.NETFramework/v4.7.2/System.Messaging.xml",
"build/.NETFramework/v4.7.2/System.Net.Http.WebRequest.dll",
"build/.NETFramework/v4.7.2/System.Net.Http.WebRequest.xml",
"build/.NETFramework/v4.7.2/System.Net.Http.dll",
"build/.NETFramework/v4.7.2/System.Net.Http.xml",
"build/.NETFramework/v4.7.2/System.Net.dll",
"build/.NETFramework/v4.7.2/System.Net.xml",
"build/.NETFramework/v4.7.2/System.Numerics.dll",
"build/.NETFramework/v4.7.2/System.Numerics.xml",
"build/.NETFramework/v4.7.2/System.Printing.dll",
"build/.NETFramework/v4.7.2/System.Printing.xml",
"build/.NETFramework/v4.7.2/System.Reflection.Context.dll",
"build/.NETFramework/v4.7.2/System.Reflection.Context.xml",
"build/.NETFramework/v4.7.2/System.Runtime.Caching.dll",
"build/.NETFramework/v4.7.2/System.Runtime.Caching.xml",
"build/.NETFramework/v4.7.2/System.Runtime.DurableInstancing.dll",
"build/.NETFramework/v4.7.2/System.Runtime.DurableInstancing.xml",
"build/.NETFramework/v4.7.2/System.Runtime.Remoting.dll",
"build/.NETFramework/v4.7.2/System.Runtime.Remoting.xml",
"build/.NETFramework/v4.7.2/System.Runtime.Serialization.Formatters.Soap.dll",
"build/.NETFramework/v4.7.2/System.Runtime.Serialization.Formatters.Soap.xml",
"build/.NETFramework/v4.7.2/System.Runtime.Serialization.dll",
"build/.NETFramework/v4.7.2/System.Runtime.Serialization.xml",
"build/.NETFramework/v4.7.2/System.Security.dll",
"build/.NETFramework/v4.7.2/System.Security.xml",
"build/.NETFramework/v4.7.2/System.ServiceModel.Activation.dll",
"build/.NETFramework/v4.7.2/System.ServiceModel.Activation.xml",
"build/.NETFramework/v4.7.2/System.ServiceModel.Activities.dll",
"build/.NETFramework/v4.7.2/System.ServiceModel.Activities.xml",
"build/.NETFramework/v4.7.2/System.ServiceModel.Channels.dll",
"build/.NETFramework/v4.7.2/System.ServiceModel.Channels.xml",
"build/.NETFramework/v4.7.2/System.ServiceModel.Discovery.dll",
"build/.NETFramework/v4.7.2/System.ServiceModel.Discovery.xml",
"build/.NETFramework/v4.7.2/System.ServiceModel.Routing.dll",
"build/.NETFramework/v4.7.2/System.ServiceModel.Routing.xml",
"build/.NETFramework/v4.7.2/System.ServiceModel.Web.dll",
"build/.NETFramework/v4.7.2/System.ServiceModel.Web.xml",
"build/.NETFramework/v4.7.2/System.ServiceModel.dll",
"build/.NETFramework/v4.7.2/System.ServiceModel.xml",
"build/.NETFramework/v4.7.2/System.ServiceProcess.dll",
"build/.NETFramework/v4.7.2/System.ServiceProcess.xml",
"build/.NETFramework/v4.7.2/System.Speech.dll",
"build/.NETFramework/v4.7.2/System.Speech.xml",
"build/.NETFramework/v4.7.2/System.Threading.Tasks.Dataflow.xml",
"build/.NETFramework/v4.7.2/System.Transactions.dll",
"build/.NETFramework/v4.7.2/System.Transactions.xml",
"build/.NETFramework/v4.7.2/System.Web.Abstractions.dll",
"build/.NETFramework/v4.7.2/System.Web.ApplicationServices.dll",
"build/.NETFramework/v4.7.2/System.Web.ApplicationServices.xml",
"build/.NETFramework/v4.7.2/System.Web.DataVisualization.Design.dll",
"build/.NETFramework/v4.7.2/System.Web.DataVisualization.dll",
"build/.NETFramework/v4.7.2/System.Web.DataVisualization.xml",
"build/.NETFramework/v4.7.2/System.Web.DynamicData.Design.dll",
"build/.NETFramework/v4.7.2/System.Web.DynamicData.Design.xml",
"build/.NETFramework/v4.7.2/System.Web.DynamicData.dll",
"build/.NETFramework/v4.7.2/System.Web.DynamicData.xml",
"build/.NETFramework/v4.7.2/System.Web.Entity.Design.dll",
"build/.NETFramework/v4.7.2/System.Web.Entity.Design.xml",
"build/.NETFramework/v4.7.2/System.Web.Entity.dll",
"build/.NETFramework/v4.7.2/System.Web.Entity.xml",
"build/.NETFramework/v4.7.2/System.Web.Extensions.Design.dll",
"build/.NETFramework/v4.7.2/System.Web.Extensions.Design.xml",
"build/.NETFramework/v4.7.2/System.Web.Extensions.dll",
"build/.NETFramework/v4.7.2/System.Web.Extensions.xml",
"build/.NETFramework/v4.7.2/System.Web.Mobile.dll",
"build/.NETFramework/v4.7.2/System.Web.Mobile.xml",
"build/.NETFramework/v4.7.2/System.Web.RegularExpressions.dll",
"build/.NETFramework/v4.7.2/System.Web.RegularExpressions.xml",
"build/.NETFramework/v4.7.2/System.Web.Routing.dll",
"build/.NETFramework/v4.7.2/System.Web.Services.dll",
"build/.NETFramework/v4.7.2/System.Web.Services.xml",
"build/.NETFramework/v4.7.2/System.Web.dll",
"build/.NETFramework/v4.7.2/System.Web.xml",
"build/.NETFramework/v4.7.2/System.Windows.Controls.Ribbon.dll",
"build/.NETFramework/v4.7.2/System.Windows.Controls.Ribbon.xml",
"build/.NETFramework/v4.7.2/System.Windows.Forms.DataVisualization.Design.dll",
"build/.NETFramework/v4.7.2/System.Windows.Forms.DataVisualization.dll",
"build/.NETFramework/v4.7.2/System.Windows.Forms.DataVisualization.xml",
"build/.NETFramework/v4.7.2/System.Windows.Forms.dll",
"build/.NETFramework/v4.7.2/System.Windows.Forms.xml",
"build/.NETFramework/v4.7.2/System.Windows.Input.Manipulations.dll",
"build/.NETFramework/v4.7.2/System.Windows.Input.Manipulations.xml",
"build/.NETFramework/v4.7.2/System.Windows.Presentation.dll",
"build/.NETFramework/v4.7.2/System.Windows.Presentation.xml",
"build/.NETFramework/v4.7.2/System.Windows.dll",
"build/.NETFramework/v4.7.2/System.Workflow.Activities.dll",
"build/.NETFramework/v4.7.2/System.Workflow.Activities.xml",
"build/.NETFramework/v4.7.2/System.Workflow.ComponentModel.dll",
"build/.NETFramework/v4.7.2/System.Workflow.ComponentModel.xml",
"build/.NETFramework/v4.7.2/System.Workflow.Runtime.dll",
"build/.NETFramework/v4.7.2/System.Workflow.Runtime.xml",
"build/.NETFramework/v4.7.2/System.WorkflowServices.dll",
"build/.NETFramework/v4.7.2/System.WorkflowServices.xml",
"build/.NETFramework/v4.7.2/System.Xaml.dll",
"build/.NETFramework/v4.7.2/System.Xaml.xml",
"build/.NETFramework/v4.7.2/System.Xml.Linq.dll",
"build/.NETFramework/v4.7.2/System.Xml.Linq.xml",
"build/.NETFramework/v4.7.2/System.Xml.Serialization.dll",
"build/.NETFramework/v4.7.2/System.Xml.dll",
"build/.NETFramework/v4.7.2/System.Xml.xml",
"build/.NETFramework/v4.7.2/System.dll",
"build/.NETFramework/v4.7.2/System.xml",
"build/.NETFramework/v4.7.2/UIAutomationClient.dll",
"build/.NETFramework/v4.7.2/UIAutomationClient.xml",
"build/.NETFramework/v4.7.2/UIAutomationClientsideProviders.dll",
"build/.NETFramework/v4.7.2/UIAutomationClientsideProviders.xml",
"build/.NETFramework/v4.7.2/UIAutomationProvider.dll",
"build/.NETFramework/v4.7.2/UIAutomationProvider.xml",
"build/.NETFramework/v4.7.2/UIAutomationTypes.dll",
"build/.NETFramework/v4.7.2/UIAutomationTypes.xml",
"build/.NETFramework/v4.7.2/WindowsBase.dll",
"build/.NETFramework/v4.7.2/WindowsBase.xml",
"build/.NETFramework/v4.7.2/WindowsFormsIntegration.dll",
"build/.NETFramework/v4.7.2/WindowsFormsIntegration.xml",
"build/.NETFramework/v4.7.2/XamlBuildTask.dll",
"build/.NETFramework/v4.7.2/XamlBuildTask.xml",
"build/.NETFramework/v4.7.2/mscorlib.dll",
"build/.NETFramework/v4.7.2/mscorlib.xml",
"build/.NETFramework/v4.7.2/namespaces.xml",
"build/.NETFramework/v4.7.2/sysglobl.dll",
"build/.NETFramework/v4.7.2/sysglobl.xml",
"build/Microsoft.NETFramework.ReferenceAssemblies.net472.targets",
"microsoft.netframework.referenceassemblies.net472.1.0.3.nupkg.sha512",
"microsoft.netframework.referenceassemblies.net472.nuspec"
]
}
},
"projectFileDependencyGroups": {
".NETFramework,Version=v4.7.2": [
"Microsoft.NETFramework.ReferenceAssemblies >= 1.0.3"
]
},
"packageFolders": {
"C:\\Users\\gruessing\\.nuget\\packages\\": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
"projectName": "DomainJoinTool",
"projectPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
"packagesPath": "C:\\Users\\gruessing\\.nuget\\packages\\",
"outputPath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\gruessing\\AppData\\Roaming\\NuGet\\NuGet.Config"
],
"originalTargetFrameworks": [
"net472"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net472": {
"targetAlias": "net472",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
}
},
"frameworks": {
"net472": {
"targetAlias": "net472",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"suppressParent": "All",
"target": "Package",
"version": "[1.0.3, )",
"autoReferenced": true
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.421\\RuntimeIdentifierGraph.json"
}
},
"runtimes": {
"win7-x64": {
"#import": []
}
}
}
}

View File

@@ -1,11 +0,0 @@
{
"version": 2,
"dgSpecHash": "nUWqqbOYUIM=",
"success": true,
"projectFilePath": "C:\\Users\\gruessing\\OneDrive - Cereda Systems GmbH\\Desktop\\IT Tool\\domain-join-tool\\DomainJoinTool.csproj",
"expectedPackageFiles": [
"C:\\Users\\gruessing\\.nuget\\packages\\microsoft.netframework.referenceassemblies\\1.0.3\\microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
"C:\\Users\\gruessing\\.nuget\\packages\\microsoft.netframework.referenceassemblies.net472\\1.0.3\\microsoft.netframework.referenceassemblies.net472.1.0.3.nupkg.sha512"
],
"logs": []
}

BIN
files.zip

Binary file not shown.

View File

@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"axios": "^1.6.5",
"dompurify": "^3.4.11",
"html5-qrcode": "^2.3.8",
"marked": "^17.0.4",
"react": "^18.2.0",
@@ -3958,7 +3959,7 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/ws": {
@@ -7074,6 +7075,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
"integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",

View File

@@ -5,6 +5,7 @@
"private": true,
"dependencies": {
"axios": "^1.6.5",
"dompurify": "^3.4.11",
"html5-qrcode": "^2.3.8",
"marked": "^17.0.4",
"react": "^18.2.0",

View File

@@ -1234,21 +1234,16 @@ button, input, textarea, select { font: inherit; color: inherit; }
let shares = [];
let demoMode = false;
/* ---------- Auth token ---------- */
const TOKEN_KEY = "token";
const getToken = () => localStorage.getItem(TOKEN_KEY);
const setToken = (t) => localStorage.setItem(TOKEN_KEY, t);
const clearToken = () => localStorage.removeItem(TOKEN_KEY);
/* ---------- Auth (httpOnly-Cookie, kein Token in localStorage/URL) ---------- */
let demoModeFlag = false;
/* ---------- Fetch helper ---------- */
async function api(path, opts = {}) {
const headers = new Headers(opts.headers || {});
const tk = getToken();
if (tk) headers.set("Authorization", "Bearer " + tk);
if (opts.body && !(opts.body instanceof FormData) && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const res = await fetch(path, { ...opts, headers });
const res = await fetch(path, { ...opts, headers, credentials: "include" });
if (!res.ok) {
const err = new Error("HTTP " + res.status);
err.status = res.status;
@@ -1259,19 +1254,6 @@ button, input, textarea, select { font: inherit; color: inherit; }
return ct.includes("application/json") ? res.json() : res.text();
}
/* ---------- Capture ?token= from URL ---------- */
function captureUrlToken() {
const params = new URLSearchParams(location.search);
const t = params.get("token");
if (t) {
setToken(t);
params.delete("token");
const qs = params.toString();
const newUrl = location.pathname + (qs ? "?" + qs : "") + location.hash;
history.replaceState(null, "", newUrl);
}
}
/* ---------- Theme toggle ---------- */
(function initTheme() {
const KEY = "cereda-theme";
@@ -1290,17 +1272,12 @@ button, input, textarea, select { font: inherit; color: inherit; }
/* ---------- Init ---------- */
async function init() {
captureUrlToken();
const tk = getToken();
if (!tk) return showLogin();
try {
const me = await api("/api/auth/me");
showApp(me);
await loadShares();
} catch (e) {
if (e.status === 401 || e.status === 403) {
clearToken();
showLogin();
} else {
enterDemoMode();
@@ -1311,7 +1288,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
/* ---------- Demo fallback ---------- */
function enterDemoMode() {
demoMode = true;
setToken("demo-token");
demoModeFlag = true;
showApp({ username: "m.schmidt", display_name: "Marco Schmidt" });
demoBadge.classList.add("on");
shares = seedShares();
@@ -1391,12 +1368,11 @@ button, input, textarea, select { font: inherit; color: inherit; }
method: "POST",
body: JSON.stringify({ username, password })
});
if (res && res.token) {
setToken(res.token);
if (res && res.status === "success") {
const me = await api("/api/auth/me").catch(() => null);
showApp(me || { username });
await loadShares();
} else throw new Error("No token in response.");
} else throw new Error("Login fehlgeschlagen.");
} catch (err) {
if (err.status === 401 || err.status === 403) {
// Real auth rejection — show the error
@@ -1413,7 +1389,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
/* ---------- Logout ---------- */
logoutBtn.addEventListener("click", () => {
clearToken();
if (!demoModeFlag) { api("/api/auth/logout", { method: "POST" }).catch(() => {}); }
shares = [];
pickedFile = null;
demoMode = false;
@@ -1593,7 +1569,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
const list = await api("/api/shares");
shares = Array.isArray(list) ? list : (list?.shares || []);
} catch (e) {
if (e.status === 401) { clearToken(); showLogin(); return; }
if (e.status === 401) { showLogin(); return; }
// 403 = not admin, just show empty list — user can still create shares
shares = [];
}

View File

@@ -2,17 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import aiService from '../../services/aiService';
import { marked } from 'marked';
marked.use({ breaks: true, gfm: true });
const renderMd = (text) => {
try {
const html = marked.parse(String(text || ''), { async: false });
return { __html: typeof html === 'string' ? html : String(html) };
} catch {
return { __html: String(text || '').replace(/\n/g, '<br>') };
}
};
import { renderMd } from '../../utils/sanitizeMarkdown';
const STAFF_ROLES = ['super_admin', 'admin', 'support', 'bearbeiter'];

View File

@@ -20,9 +20,8 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
const connectedRef = useRef(false);
const getWsUrl = () => {
const token = localStorage.getItem('token');
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
return `${proto}://${window.location.host}/ws?type=rdp&agentId=${agentId}&token=${token}`;
return `${proto}://${window.location.host}/ws?type=rdp&agentId=${agentId}`;
};
const connect = (screen = screenIdx) => {
@@ -33,6 +32,7 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
connectedRef.current = false;
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: localStorage.getItem('token') }));
ws.send(JSON.stringify({ type: 'rdp_start', screen }));
};
@@ -61,7 +61,9 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
img.src = 'data:image/jpeg;base64,' + msg.data;
} else if (msg.type === 'rdp_denied') {
setStatus('error');
setError('Zugriff wurde vom Benutzer abgelehnt');
setError(msg.reason === 'consent_spawn_failed'
? 'Consent-Dialog konnte nicht geöffnet werden (schtasks-Fehler)'
: 'Zugriff wurde vom Benutzer abgelehnt');
ws.close();
} else if (msg.type === 'rdp_disconnected') {
setStatus('idle');
@@ -119,8 +121,14 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
{/* Screen-Selektor */}
<select
value={screenIdx}
onChange={e => setScreenIdx(Number(e.target.value))}
disabled={status === 'connected' || status === 'connecting' || status === 'waiting'}
onChange={e => {
const newIdx = Number(e.target.value);
setScreenIdx(newIdx);
if (status === 'connected' && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: 'rdp_switch_screen', screen: newIdx }));
}
}}
disabled={status === 'connecting' || status === 'waiting'}
style={{ fontSize: 12, padding: '3px 8px', borderRadius: 7, border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'var(--text-primary)', cursor: 'pointer', marginLeft: 4 }}
>
{SCREEN_OPTIONS.map(o => (

View File

@@ -177,10 +177,9 @@ function RemoteShell({ agentId, agentHostname }) {
const inputRef = useRef(null);
const getWsUrl = () => {
const token = localStorage.getItem('token');
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const host = window.location.host;
return `${proto}://${host}/ws?type=shell&agentId=${agentId}&token=${token}`;
return `${proto}://${host}/ws?type=shell&agentId=${agentId}`;
};
const connect = () => {
@@ -191,7 +190,10 @@ function RemoteShell({ agentId, agentHostname }) {
const ws = new WebSocket(getWsUrl());
wsRef.current = ws;
ws.onopen = () => setStatus('connected');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: localStorage.getItem('token') }));
setStatus('connected');
};
ws.onmessage = (e) => {
setOutput(prev => prev + stripAnsi(e.data));
@@ -766,7 +768,7 @@ export default function AgentDetailPage() {
</div>
{/* ── REMOTE TOOLS ─────────────────────────────────────────────────── */}
{(isSuperAdmin || isAdmin) && (
{(isSuperAdmin() || isAdmin()) && (
<div style={{ marginTop: 24 }}>
<div style={{ display: 'flex', gap: 4, marginBottom: 0, borderBottom: '1px solid var(--border-color)' }}>
{[
@@ -790,10 +792,6 @@ export default function AgentDetailPage() {
{shellTab === 'rdp' && <RemoteDesktop agentId={agent.id} agentHostname={agent.hostname} />}
</div>
)}
{!isSuperAdmin && !isAdmin && (
<RemoteShell agentId={agent.id} agentHostname={agent.hostname} />
)}
{/* ── ANKÜNDIGUNG MODAL ────────────────────────────────────────────── */}
{showAnnModal && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.6)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}

View File

@@ -2,10 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '../context/AuthContext';
import aiService from '../services/aiService';
import { toast } from 'react-toastify';
import { Marked } from 'marked';
const marked = new Marked({ breaks: true, gfm: true });
const renderMd = (text) => ({ __html: marked.parse(text) });
import { renderMd } from '../utils/sanitizeMarkdown';
const WELCOME_MSG = {
role: 'assistant',

Some files were not shown because too many files have changed in this diff Show More