Compare commits
16 Commits
58d7df64e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b1e0bd9548 | |||
| 07fa8d6020 | |||
| 33e0b73f7f | |||
| f3bd8d0910 | |||
| d513992de7 | |||
| 9a5ed02a8c | |||
| 81b1c326fc | |||
| 3ea28def4c | |||
| cb59cfe9cf | |||
| e2521f405d | |||
| 7449b64398 | |||
| 99830e25f4 | |||
| 0b2943087d | |||
| c276b23293 | |||
| 54114b5b9f | |||
| 50bbfc6b22 |
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
26
.env.example
Normal 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=
|
||||||
64
.github/workflows/README.md
vendored
64
.github/workflows/README.md
vendored
@@ -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**.
|
|
||||||
68
.github/workflows/deploy.yml
vendored
68
.github/workflows/deploy.yml
vendored
@@ -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
17
.gitignore
vendored
@@ -35,6 +35,7 @@ agent/*.exe
|
|||||||
agent/*.msi
|
agent/*.msi
|
||||||
agent-cs/bin/
|
agent-cs/bin/
|
||||||
agent-cs/obj/
|
agent-cs/obj/
|
||||||
|
agent-cs/publish/
|
||||||
|
|
||||||
# Uploads
|
# Uploads
|
||||||
backend/uploads/
|
backend/uploads/
|
||||||
@@ -47,7 +48,15 @@ installer/
|
|||||||
*.intunewin
|
*.intunewin
|
||||||
*.ova
|
*.ova
|
||||||
*.exe
|
*.exe
|
||||||
installer/
|
|
||||||
*.intunewin
|
# Nicht Teil der IT-Nexus-App (separate Tools/Demos/Scratch — nur lokal)
|
||||||
*.ova
|
.claude/
|
||||||
*.exe
|
.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 |
@@ -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.
|
|
||||||
1524
Assets-Demo.html
1524
Assets-Demo.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
29
IT Tool.sln
29
IT Tool.sln
@@ -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
@@ -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.*
|
|
||||||
1074
Tickets-Demo.html
1074
Tickets-Demo.html
File diff suppressed because it is too large
Load Diff
@@ -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*
|
|
||||||
@@ -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
1
_ul
@@ -1 +0,0 @@
|
|||||||
mkdir: cannot create directory ‘C:\\gradle-home\\it_nexus_build’: File exists
|
|
||||||
@@ -6,7 +6,7 @@ namespace ITNexusAgent;
|
|||||||
|
|
||||||
public class AgentWorker
|
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 DataDir = @"C:\ProgramData\IT Nexus Agent";
|
||||||
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
||||||
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.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;
|
_exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
||||||
Directory.CreateDirectory(DataDir);
|
Directory.CreateDirectory(DataDir);
|
||||||
|
SecureDataDir(DataDir);
|
||||||
|
|
||||||
if (!File.Exists(ConfigPath))
|
if (!File.Exists(ConfigPath))
|
||||||
{
|
{
|
||||||
@@ -39,12 +40,25 @@ public class AgentWorker
|
|||||||
|
|
||||||
Log($"Agent v{Version} gestartet");
|
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
|
// 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);
|
_ = shellService.RunAsync(_ct);
|
||||||
|
|
||||||
// WebRTC Remote Desktop Service im Hintergrund starten
|
// 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);
|
_ = rtcService.RunAsync(_ct);
|
||||||
|
|
||||||
while (!_ct.IsCancellationRequested)
|
while (!_ct.IsCancellationRequested)
|
||||||
@@ -176,6 +190,30 @@ public class AgentWorker
|
|||||||
catch { }
|
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)
|
private static void SecureConfigFile(string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public static class CaptureModeRunner
|
|||||||
private static readonly ImageCodecInfo JpegCodec =
|
private static readonly ImageCodecInfo JpegCodec =
|
||||||
ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
|
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;
|
if (!int.TryParse(portStr, out var port) || port <= 0) return;
|
||||||
|
|
||||||
@@ -20,11 +20,11 @@ public static class CaptureModeRunner
|
|||||||
using var tcp = new TcpClient();
|
using var tcp = new TcpClient();
|
||||||
tcp.Connect("127.0.0.1", port);
|
tcp.Connect("127.0.0.1", port);
|
||||||
var stream = tcp.GetStream();
|
var stream = tcp.GetStream();
|
||||||
|
stream.Write(System.Text.Encoding.ASCII.GetBytes(secret));
|
||||||
|
|
||||||
var encParams = new EncoderParameters(1);
|
var encParams = new EncoderParameters(1);
|
||||||
encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L);
|
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;
|
var allScreens = System.Windows.Forms.Screen.AllScreens;
|
||||||
Rectangle captureRect;
|
Rectangle captureRect;
|
||||||
if (screenIdx <= 0 || screenIdx > allScreens.Length)
|
if (screenIdx <= 0 || screenIdx > allScreens.Length)
|
||||||
@@ -40,14 +40,27 @@ public static class CaptureModeRunner
|
|||||||
captureRect = allScreens[screenIdx - 1].Bounds;
|
captureRect = allScreens[screenIdx - 1].Bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint lastHash = 0;
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
while (tcp.Connected)
|
while (tcp.Connected)
|
||||||
{
|
{
|
||||||
|
sw.Restart();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var bmp = new Bitmap(captureRect.Width, captureRect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
using var bmp = new Bitmap(captureRect.Width, captureRect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
||||||
using (var g = Graphics.FromImage(bmp))
|
using (var g = Graphics.FromImage(bmp))
|
||||||
g.CopyFromScreen(captureRect.X, captureRect.Y, 0, 0, new Size(captureRect.Width, captureRect.Height));
|
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;
|
byte[] jpeg;
|
||||||
using (var ms = new MemoryStream())
|
using (var ms = new MemoryStream())
|
||||||
{
|
{
|
||||||
@@ -55,16 +68,32 @@ public static class CaptureModeRunner
|
|||||||
jpeg = ms.ToArray();
|
jpeg = ms.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4-Byte Länge (LE) + JPEG-Daten
|
|
||||||
stream.Write(BitConverter.GetBytes(jpeg.Length));
|
stream.Write(BitConverter.GetBytes(jpeg.Length));
|
||||||
stream.Write(jpeg);
|
stream.Write(jpeg);
|
||||||
stream.Flush();
|
stream.Flush();
|
||||||
}
|
}
|
||||||
catch { break; }
|
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 */ }
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ namespace ITNexusAgent;
|
|||||||
// Läuft als User-Prozess (via schtasks), zeigt Consent-Dialog und sendet Antwort via TCP
|
// Läuft als User-Prozess (via schtasks), zeigt Consent-Dialog und sendet Antwort via TCP
|
||||||
public static class ConsentModeRunner
|
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;
|
if (!int.TryParse(portStr, out var port) || port <= 0) return;
|
||||||
|
|
||||||
var app = new System.Windows.Application();
|
var app = new System.Windows.Application();
|
||||||
app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
|
app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
|
||||||
|
|
||||||
var win = new RdpConsentWindow(port);
|
var win = new RdpConsentWindow(port, secret);
|
||||||
win.Topmost = true;
|
win.Topmost = true;
|
||||||
win.Show();
|
win.Show();
|
||||||
win.Activate();
|
win.Activate();
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<AssemblyName>IT-Nexus-Agent</AssemblyName>
|
<AssemblyName>IT-Nexus-Agent</AssemblyName>
|
||||||
<RootNamespace>ITNexusAgent</RootNamespace>
|
<RootNamespace>ITNexusAgent</RootNamespace>
|
||||||
<Version>2.0.0</Version>
|
<Version>2.9.0</Version>
|
||||||
<AssemblyVersion>2.0.0.0</AssemblyVersion>
|
<AssemblyVersion>2.9.0.0</AssemblyVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
20
agent-cs/IndicatorModeRunner.cs
Normal file
20
agent-cs/IndicatorModeRunner.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,69 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace ITNexusAgent.Models;
|
namespace ITNexusAgent.Models;
|
||||||
|
|
||||||
public class AgentConfig
|
public class AgentConfig
|
||||||
{
|
{
|
||||||
[JsonProperty("server_url")]
|
private const string ProtectedPrefix = "dpapi:";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public string ServerUrl { get; set; } = "";
|
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; } = "";
|
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)
|
public static AgentConfig Load(string path)
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(path);
|
var json = File.ReadAllText(path);
|
||||||
return JsonConvert.DeserializeObject<AgentConfig>(json)
|
var raw = JsonConvert.DeserializeObject<RawConfig>(json)
|
||||||
?? throw new Exception("Ungültige config.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,22 @@ internal class Program
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
case "--rdp-consent":
|
case "--rdp-consent":
|
||||||
ConsentModeRunner.Run(args.Length > 1 ? args[1] : "");
|
ConsentModeRunner.Run(
|
||||||
|
args.Length > 1 ? args[1] : "",
|
||||||
|
args.Length > 2 ? args[2] : "");
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case "--rdp-capture":
|
case "--rdp-capture":
|
||||||
CaptureModeRunner.Run(
|
CaptureModeRunner.Run(
|
||||||
args.Length > 1 ? args[1] : "",
|
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;
|
return;
|
||||||
|
|
||||||
case "--dashboard":
|
case "--dashboard":
|
||||||
|
|||||||
@@ -9,7 +9,25 @@ public class ApiService(string serverUrl, string agentKey)
|
|||||||
{
|
{
|
||||||
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) };
|
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) };
|
||||||
private readonly string _baseUrl = serverUrl.TrimEnd('/');
|
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)
|
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();
|
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);
|
var resp = await _http.SendAsync(req);
|
||||||
resp.EnsureSuccessStatusCode();
|
resp.EnsureSuccessStatusCode();
|
||||||
return await resp.Content.ReadAsByteArrayAsync();
|
return await resp.Content.ReadAsByteArrayAsync();
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ public class CommandExecutor(ApiService api, string hostname, string dataDir, st
|
|||||||
|
|
||||||
private async Task<string> UpdateAgent()
|
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";
|
if (bytes.Length < 512 * 1024) return "Download fehlgeschlagen - Installer zu klein";
|
||||||
|
|
||||||
var setupPath = Path.Combine(_dataDir, "IT-Nexus-Agent-Setup-Update.exe");
|
var setupPath = Path.Combine(_dataDir, "IT-Nexus-Agent-Setup-Update.exe");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -15,6 +16,9 @@ public class RtcService
|
|||||||
private readonly string _serverUrl;
|
private readonly string _serverUrl;
|
||||||
private readonly string _agentKey;
|
private readonly string _agentKey;
|
||||||
private readonly string _hostname;
|
private readonly string _hostname;
|
||||||
|
private int _indicatorPid = -1;
|
||||||
|
private TcpListener? _indicatorListener;
|
||||||
|
private CancellationTokenSource? _userDisconnectCts;
|
||||||
|
|
||||||
public RtcService(string serverUrl, string agentKey, string hostname)
|
public RtcService(string serverUrl, string agentKey, string hostname)
|
||||||
{
|
{
|
||||||
@@ -70,31 +74,81 @@ public class RtcService
|
|||||||
_ = ConsentAndCaptureAsync(ws, screenIdx, captureCts.Token);
|
_ = ConsentAndCaptureAsync(ws, screenIdx, captureCts.Token);
|
||||||
AgentWorker.Log($"RDP: Consent angefordert (screen={screenIdx})");
|
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)
|
else if (type == "rdp_stop" && captureCts != null)
|
||||||
{
|
{
|
||||||
captureCts.Cancel();
|
captureCts.Cancel();
|
||||||
captureCts = null;
|
captureCts = null;
|
||||||
|
StopIndicator();
|
||||||
AgentWorker.Log("RDP: Screen-Capture gestoppt");
|
AgentWorker.Log("RDP: Screen-Capture gestoppt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
captureCts?.Cancel();
|
captureCts?.Cancel();
|
||||||
|
StopIndicator();
|
||||||
AgentWorker.Log("RDP: Getrennt");
|
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)
|
private async Task ConsentAndCaptureAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
||||||
|
var secret = GenerateSecret();
|
||||||
|
|
||||||
var consentListener = new TcpListener(IPAddress.Loopback, 0);
|
var consentListener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
consentListener.Start();
|
consentListener.Start();
|
||||||
var consentPort = ((IPEndPoint)consentListener.LocalEndpoint).Port;
|
var consentPort = ((IPEndPoint)consentListener.LocalEndpoint).Port;
|
||||||
|
|
||||||
if (!SpawnConsentHelper(exePath, consentPort.ToString()))
|
if (!SpawnConsentHelper(exePath, consentPort.ToString(), secret))
|
||||||
{
|
{
|
||||||
consentListener.Stop();
|
consentListener.Stop();
|
||||||
AgentWorker.Log("RDP: Consent-Helper fehlgeschlagen, starte ohne Consent");
|
AgentWorker.Log("RDP: Consent-Helper fehlgeschlagen, verweigere Zugriff");
|
||||||
await CapturePipeLoopAsync(ws, screenIdx, ct);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,9 +162,15 @@ public class RtcService
|
|||||||
{
|
{
|
||||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(35));
|
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(35));
|
||||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
|
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
|
||||||
using var tcp = await consentListener.AcceptTcpClientAsync(linked.Token);
|
while (!linked.IsCancellationRequested)
|
||||||
var b = tcp.GetStream().ReadByte();
|
{
|
||||||
accepted = b == 1;
|
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; }
|
catch { accepted = false; }
|
||||||
finally { consentListener.Stop(); }
|
finally { consentListener.Stop(); }
|
||||||
@@ -125,72 +185,71 @@ public class RtcService
|
|||||||
}
|
}
|
||||||
|
|
||||||
AgentWorker.Log("RDP: User hat Zugriff erlaubt, starte Capture");
|
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
|
try
|
||||||
{
|
{
|
||||||
var fullUser = NotificationService.GetLoggedOnUser();
|
_indicatorListener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
if (string.IsNullOrEmpty(fullUser))
|
_indicatorListener.Start();
|
||||||
{
|
var port = ((IPEndPoint)_indicatorListener.LocalEndpoint).Port;
|
||||||
AgentWorker.Log("RDP: Kein eingeloggter User für Consent");
|
var secret = GenerateSecret();
|
||||||
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();
|
|
||||||
|
|
||||||
|
_userDisconnectCts = new CancellationTokenSource();
|
||||||
|
var listener = _indicatorListener;
|
||||||
|
var disconnectCts = _userDisconnectCts;
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await Task.Delay(40000);
|
try
|
||||||
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
|
{
|
||||||
{ CreateNoWindow = true })?.WaitForExit();
|
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}'");
|
_indicatorPid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-indicator {port} {secret}");
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
AgentWorker.Log($"RDP: SpawnConsentHelper Fehler: {ex.Message}");
|
AgentWorker.Log($"RDP: Indicator-Start fehlgeschlagen: {ex.Message}");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
||||||
|
var secret = GenerateSecret();
|
||||||
|
|
||||||
// TCP Loopback: kein ACL-Problem zwischen SYSTEM-Service und User-Prozess
|
// TCP Loopback: kein ACL-Problem zwischen SYSTEM-Service und User-Prozess
|
||||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
listener.Start();
|
listener.Start();
|
||||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
|
||||||
if (!SpawnCaptureHelper(exePath, port.ToString(), screenIdx))
|
if (!SpawnCaptureHelper(exePath, port.ToString(), screenIdx, secret))
|
||||||
{
|
{
|
||||||
listener.Stop();
|
listener.Stop();
|
||||||
AgentWorker.Log("RDP: Helper-Start fehlgeschlagen");
|
AgentWorker.Log("RDP: Helper-Start fehlgeschlagen");
|
||||||
@@ -202,7 +261,16 @@ public class RtcService
|
|||||||
{
|
{
|
||||||
using var connectCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
using var connectCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, connectCts.Token);
|
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
|
catch
|
||||||
{
|
{
|
||||||
@@ -260,58 +328,9 @@ public class RtcService
|
|||||||
AgentWorker.Log("RDP: Frame-Loop beendet");
|
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 pid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-capture {portStr} {screenIdx} {secret}");
|
||||||
{
|
return pid > 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
160
agent-cs/Services/SessionSpawner.cs
Normal file
160
agent-cs/Services/SessionSpawner.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ public class ShellService
|
|||||||
+ $"/ws?type=agent&key={Uri.EscapeDataString(_agentKey)}&hostname={Uri.EscapeDataString(_hostname)}";
|
+ $"/ws?type=agent&key={Uri.EscapeDataString(_agentKey)}&hostname={Uri.EscapeDataString(_hostname)}";
|
||||||
|
|
||||||
using var ws = new ClientWebSocket();
|
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);
|
await ws.ConnectAsync(new Uri(wsUrl), ct);
|
||||||
AgentWorker.Log("SHELL: WebSocket verbunden");
|
AgentWorker.Log("SHELL: WebSocket verbunden");
|
||||||
|
|||||||
57
agent-cs/UI/RdpActiveIndicatorWindow.xaml
Normal file
57
agent-cs/UI/RdpActiveIndicatorWindow.xaml
Normal 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>
|
||||||
60
agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs
Normal file
60
agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs
Normal 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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
<Window x:Class="ITNexusAgent.UI.RdpConsentWindow"
|
<Window x:Class="ITNexusAgent.UI.RdpConsentWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="IT Nexus – Bildschirmzugriff"
|
Title="IT Nexus – Bildschirmzugriff angefordert"
|
||||||
Width="480" Height="Auto"
|
Width="480" Height="Auto"
|
||||||
SizeToContent="Height"
|
SizeToContent="Height"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
ResizeMode="NoResize"
|
ResizeMode="NoResize"
|
||||||
WindowStyle="None"
|
WindowStyle="ToolWindow"
|
||||||
AllowsTransparency="False"
|
AllowsTransparency="False"
|
||||||
Background="#1A1D2E"
|
Background="#1A1D2E"
|
||||||
Topmost="True"
|
Topmost="True"
|
||||||
|
ShowInTaskbar="True"
|
||||||
FontFamily="Segoe UI">
|
FontFamily="Segoe UI">
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace ITNexusAgent.UI;
|
namespace ITNexusAgent.UI;
|
||||||
@@ -6,13 +7,15 @@ namespace ITNexusAgent.UI;
|
|||||||
public partial class RdpConsentWindow : Window
|
public partial class RdpConsentWindow : Window
|
||||||
{
|
{
|
||||||
private readonly int _port;
|
private readonly int _port;
|
||||||
|
private readonly string _secret;
|
||||||
private bool _answered = false;
|
private bool _answered = false;
|
||||||
private System.Threading.CancellationTokenSource _countdownCts = new();
|
private System.Threading.CancellationTokenSource _countdownCts = new();
|
||||||
|
|
||||||
public RdpConsentWindow(int port)
|
public RdpConsentWindow(int port, string secret)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_port = port;
|
_port = port;
|
||||||
|
_secret = secret;
|
||||||
_ = RunCountdownAsync(_countdownCts.Token);
|
_ = RunCountdownAsync(_countdownCts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +56,10 @@ public partial class RdpConsentWindow : Window
|
|||||||
{
|
{
|
||||||
using var tcp = new TcpClient();
|
using var tcp = new TcpClient();
|
||||||
tcp.Connect("127.0.0.1", _port);
|
tcp.Connect("127.0.0.1", _port);
|
||||||
tcp.GetStream().WriteByte((byte)(accepted ? 1 : 0));
|
var stream = tcp.GetStream();
|
||||||
tcp.GetStream().Flush();
|
stream.Write(Encoding.ASCII.GetBytes(_secret));
|
||||||
|
stream.WriteByte((byte)(accepted ? 1 : 0));
|
||||||
|
stream.Flush();
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#define MyAppName "IT Nexus Agent"
|
#define MyAppName "IT Nexus Agent"
|
||||||
#define MyAppVersion "2.2.0"
|
#define MyAppVersion "2.9.0"
|
||||||
#define MyAppPublisher "Cereda Systems GmbH"
|
#define MyAppPublisher "Cereda Systems GmbH"
|
||||||
#define MyAppURL "https://it-nexus.cereda-systems.de"
|
#define MyAppURL "https://it-nexus.cereda-systems.de"
|
||||||
#define MyAppExeName "IT-Nexus-Agent.exe"
|
#define MyAppExeName "IT-Nexus-Agent.exe"
|
||||||
@@ -64,17 +64,31 @@ Type: files; Name: "{commonappdata}\IT Nexus Agent\agent.log"
|
|||||||
procedure CurStepChanged(CurStep: TSetupStep);
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
var
|
var
|
||||||
ResultCode: Integer;
|
ResultCode: Integer;
|
||||||
|
AppData: String;
|
||||||
begin
|
begin
|
||||||
if CurStep = ssInstall then begin
|
if CurStep = ssInstall then begin
|
||||||
// Service stoppen (neue Architektur)
|
// Service stoppen (neue Architektur)
|
||||||
Exec('net.exe', 'stop "IT Nexus Agent"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
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" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
Exec('schtasks.exe', '/delete /tn "IT Nexus Agent Watcher" /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 "ITNexusAgent" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
Exec('schtasks.exe', '/delete /tn "ITNexusAgentWatcher" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
Exec('schtasks.exe', '/delete /tn "ITNexusAgentWatcher" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
// Autostart-Eintrag aus vorheriger Version entfernen (falls vorhanden)
|
// Autostart-Eintrag aus vorheriger Version entfernen (falls vorhanden)
|
||||||
RegDeleteValue(HKCU, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'IT Nexus Agent');
|
RegDeleteValue(HKCU, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'IT Nexus Agent');
|
||||||
Sleep(2000);
|
Sleep(1500);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -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 & 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 & Uptime</li>
|
|
||||||
<li class="feature-item"><span class="feature-dot"></span> Netzwerk & 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">✓</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 → Administration → 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="✓"; }
|
|
||||||
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>
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"server_url": "https://it-nexus.cereda-systems.de",
|
|
||||||
"agent_key": "DEIN-AGENT-KEY-HIER"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"server_url": "https://it-nexus.cereda-systems.de",
|
|
||||||
"agent_key": "cereda-agent-2024-secure-key"
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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)"
|
|
||||||
}
|
|
||||||
103
agent/setup.iss
103
agent/setup.iss
@@ -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;
|
|
||||||
@@ -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
|
|
||||||
20
backend/package-lock.json
generated
20
backend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"botbuilder": "^4.23.3",
|
"botbuilder": "^4.23.3",
|
||||||
"bwip-js": "^4.8.0",
|
"bwip-js": "^4.8.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
@@ -1371,6 +1372,25 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"botbuilder": "^4.23.3",
|
"botbuilder": "^4.23.3",
|
||||||
"bwip-js": "^4.8.0",
|
"bwip-js": "^4.8.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
require('dotenv').config();
|
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 = {
|
const JWT_CONFIG = {
|
||||||
secret: process.env.JWT_SECRET || 'default-secret-change-in-production',
|
secret: process.env.JWT_SECRET,
|
||||||
expiresIn: process.env.JWT_EXPIRATION || '8h',
|
expiresIn: process.env.JWT_EXPIRATION || '8h',
|
||||||
algorithm: 'HS256'
|
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;
|
module.exports = JWT_CONFIG;
|
||||||
|
|||||||
@@ -160,8 +160,12 @@ class AiController {
|
|||||||
const { url } = req.body;
|
const { url } = req.body;
|
||||||
if (!url?.trim()) throw new AppError('URL ist erforderlich', 400);
|
if (!url?.trim()) throw new AppError('URL ist erforderlich', 400);
|
||||||
|
|
||||||
// Only allow http/https
|
const { assertPublicUrl } = require('../utils/ssrfGuard');
|
||||||
if (!/^https?:\/\//i.test(url)) throw new AppError('Nur HTTP/HTTPS URLs erlaubt', 400);
|
try {
|
||||||
|
await assertPublicUrl(url);
|
||||||
|
} catch (e) {
|
||||||
|
throw new AppError(e.message, 400);
|
||||||
|
}
|
||||||
|
|
||||||
let html;
|
let html;
|
||||||
try {
|
try {
|
||||||
@@ -204,7 +208,12 @@ class AiController {
|
|||||||
const AiService = require('../services/ai.service');
|
const AiService = require('../services/ai.service');
|
||||||
const { url, maxPages = 20 } = req.body;
|
const { url, maxPages = 20 } = req.body;
|
||||||
if (!url?.trim()) throw new AppError('URL ist erforderlich', 400);
|
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 limit = Math.min(Math.max(1, parseInt(maxPages) || 20), 100);
|
||||||
const baseUrl = new URL(url);
|
const baseUrl = new URL(url);
|
||||||
@@ -236,6 +245,7 @@ class AiController {
|
|||||||
visited.add(currentUrl);
|
visited.add(currentUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
try { await assertPublicUrl(currentUrl); } catch { continue; } // DNS-Rebinding-Schutz
|
||||||
const response = await fetch(currentUrl, {
|
const response = await fetch(currentUrl, {
|
||||||
headers: { 'User-Agent': 'Mozilla/5.0 IT-Nexus KnowledgeBase Importer' },
|
headers: { 'User-Agent': 'Mozilla/5.0 IT-Nexus KnowledgeBase Importer' },
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const AuthService = require('../services/auth.service');
|
const AuthService = require('../services/auth.service');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const { asyncHandler } = require('../middleware/errorHandler');
|
const { asyncHandler } = require('../middleware/errorHandler');
|
||||||
|
const { setAuthCookie, clearAuthCookie } = require('../utils/authCookie');
|
||||||
|
|
||||||
class AuthController {
|
class AuthController {
|
||||||
/**
|
/**
|
||||||
@@ -18,10 +19,11 @@ class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await AuthService.login(username, password);
|
const result = await AuthService.login(username, password);
|
||||||
|
setAuthCookie(res, result.token);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: result
|
data: { user: result.user }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,8 +106,7 @@ class AuthController {
|
|||||||
* POST /api/auth/logout
|
* POST /api/auth/logout
|
||||||
*/
|
*/
|
||||||
static logout = asyncHandler(async (req, res) => {
|
static logout = asyncHandler(async (req, res) => {
|
||||||
// Client-side will handle token removal
|
clearAuthCookie(res);
|
||||||
// This endpoint is just for consistency and potential future server-side session handling
|
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Logged out successfully'
|
message: 'Logged out successfully'
|
||||||
|
|||||||
@@ -4,6 +4,60 @@ const { asyncHandler } = require('../middleware/errorHandler');
|
|||||||
const { getPendingCommands } = require('./patch.controller');
|
const { getPendingCommands } = require('./patch.controller');
|
||||||
const { getDatabase } = require('../config/database');
|
const { getDatabase } = require('../config/database');
|
||||||
const { getForAgent } = require('./announcement.controller');
|
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) {
|
function extractManufacturer(modelStr) {
|
||||||
if (!modelStr) return null;
|
if (!modelStr) return null;
|
||||||
@@ -75,14 +129,13 @@ function syncAgentToAsset(data) {
|
|||||||
// POST /api/monitoring/checkin — called by agent (no JWT, uses API key)
|
// POST /api/monitoring/checkin — called by agent (no JWT, uses API key)
|
||||||
const checkin = asyncHandler(async (req, res) => {
|
const checkin = asyncHandler(async (req, res) => {
|
||||||
const agentKey = req.headers['x-agent-key'];
|
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;
|
const { hostname } = req.body;
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
return res.status(400).json({ status: 'error', message: 'hostname required' });
|
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);
|
const agent = MonitoringAgent.upsert(req.body);
|
||||||
|
|
||||||
@@ -93,7 +146,7 @@ const checkin = asyncHandler(async (req, res) => {
|
|||||||
const { pending, running } = getPendingCommands(agent.id);
|
const { pending, running } = getPendingCommands(agent.id);
|
||||||
|
|
||||||
// Zielversion: Gruppen-spezifisch (staged rollout) oder globaler Default
|
// 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;
|
let targetVersion = GLOBAL_VERSION;
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
@@ -108,6 +161,8 @@ const checkin = asyncHandler(async (req, res) => {
|
|||||||
if (groupRow?.target_agent_version) targetVersion = groupRow.target_agent_version;
|
if (groupRow?.target_agent_version) targetVersion = groupRow.target_agent_version;
|
||||||
} catch { /* kein Gruppe zugewiesen → global */ }
|
} catch { /* kein Gruppe zugewiesen → global */ }
|
||||||
|
|
||||||
|
targetVersion = clampToNotOlder(targetVersion, req.body.agent_version);
|
||||||
|
|
||||||
const announcements = getForAgent(agent.id);
|
const announcements = getForAgent(agent.id);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -133,11 +188,11 @@ const getAll = asyncHandler(async (req, res) => {
|
|||||||
// POST /api/monitoring/announcements-poll — leichter Poll nur für Ankündigungen
|
// POST /api/monitoring/announcements-poll — leichter Poll nur für Ankündigungen
|
||||||
const announcementsPoll = asyncHandler(async (req, res) => {
|
const announcementsPoll = asyncHandler(async (req, res) => {
|
||||||
const agentKey = req.headers['x-agent-key'];
|
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;
|
const { hostname } = req.body;
|
||||||
if (!hostname) return res.status(400).json({ error: 'hostname required' });
|
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);
|
const agent = MonitoringAgent.getByHostname(hostname);
|
||||||
if (!agent) return res.json({ announcements: [] });
|
if (!agent) return res.json({ announcements: [] });
|
||||||
const announcements = getForAgent(agent.id);
|
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' });
|
return res.status(404).json({ status: 'error', message: 'Script nicht gefunden' });
|
||||||
}
|
}
|
||||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
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);
|
res.sendFile(scriptPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,7 +259,7 @@ const downloadAnnWatcher = asyncHandler(async (req, res) => {
|
|||||||
const downloadSetup = asyncHandler(async (req, res) => {
|
const downloadSetup = asyncHandler(async (req, res) => {
|
||||||
// Agent-Key Auth für auto-update vom Agent selbst
|
// Agent-Key Auth für auto-update vom Agent selbst
|
||||||
const agentKey = req.headers['x-agent-key'];
|
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';
|
const isAdmin = req.user?.role_name === 'admin' || req.user?.role_name === 'super_admin';
|
||||||
if (!isAgent && !isAdmin) {
|
if (!isAgent && !isAdmin) {
|
||||||
return res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
return res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||||
@@ -212,7 +267,27 @@ const downloadSetup = asyncHandler(async (req, res) => {
|
|||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
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`);
|
const setupPath = path.join(__dirname, `../../agent/IT-Nexus-Agent-Setup-v${version}.exe`);
|
||||||
if (!fs.existsSync(setupPath)) {
|
if (!fs.existsSync(setupPath)) {
|
||||||
return res.status(404).json({ status: 'error', message: 'Setup nicht gefunden' });
|
return res.status(404).json({ status: 'error', message: 'Setup nicht gefunden' });
|
||||||
@@ -223,4 +298,4 @@ const downloadSetup = asyncHandler(async (req, res) => {
|
|||||||
res.sendFile(setupPath);
|
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 };
|
||||||
|
|||||||
@@ -1,4 +1,72 @@
|
|||||||
const { getDatabase } = require('../config/database');
|
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) {
|
async function getStats(req, res) {
|
||||||
try {
|
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 };
|
||||||
|
|||||||
@@ -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_enabled INTEGER DEFAULT 0`,
|
||||||
`ALTER TABLE monitoring_agents ADD COLUMN defender_signatures_age INTEGER DEFAULT -1`,
|
`ALTER TABLE monitoring_agents ADD COLUMN defender_signatures_age INTEGER DEFAULT -1`,
|
||||||
`ALTER TABLE monitoring_agents ADD COLUMN hardware_serial TEXT`,
|
`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)
|
// Sichere Links (Shares)
|
||||||
`CREATE TABLE IF NOT EXISTS shares (
|
`CREATE TABLE IF NOT EXISTS shares (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
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 last_used_at DATETIME`,
|
||||||
`ALTER TABLE fido_keys ADD COLUMN manufacturer TEXT`,
|
`ALTER TABLE fido_keys ADD COLUMN manufacturer TEXT`,
|
||||||
`ALTER TABLE fido_keys ADD COLUMN connection_type TEXT`,
|
`ALTER TABLE fido_keys ADD COLUMN connection_type TEXT`,
|
||||||
|
`ALTER TABLE fido_keys ADD COLUMN pin TEXT`,
|
||||||
// Asset-Agent-Sync
|
// Asset-Agent-Sync
|
||||||
`ALTER TABLE assets ADD COLUMN os TEXT`,
|
`ALTER TABLE assets ADD COLUMN os TEXT`,
|
||||||
`ALTER TABLE assets ADD COLUMN ip_address TEXT`,
|
`ALTER TABLE assets ADD COLUMN ip_address TEXT`,
|
||||||
@@ -552,6 +555,23 @@ async function initializeDatabase() {
|
|||||||
}
|
}
|
||||||
console.log('✅ Database migrations completed');
|
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
|
// Special migration: rebuild network_devices to add SNMP + AP support
|
||||||
try {
|
try {
|
||||||
const ndDef = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='network_devices'").get();
|
const ndDef = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='network_devices'").get();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const JWT_CONFIG = require('../config/jwt');
|
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
|
* Middleware to verify JWT token and attach user to request
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +21,19 @@ function authenticateToken(req, res, next) {
|
|||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, JWT_CONFIG.secret);
|
const decoded = jwt.verify(token, JWT_CONFIG.secret);
|
||||||
req.user = decoded; // { id, username, email, role, roleId }
|
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();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'TokenExpiredError') {
|
if (error.name === 'TokenExpiredError') {
|
||||||
|
|||||||
@@ -105,6 +105,16 @@ class AuditLog {
|
|||||||
return stmt.all(limit);
|
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
|
* Helper method to log user actions
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
const { getDatabase } = require('../config/database');
|
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 {
|
class FidoKey {
|
||||||
/**
|
/**
|
||||||
@@ -19,7 +25,7 @@ class FidoKey {
|
|||||||
LEFT JOIN users uu ON fk.updated_by_user_id = uu.id
|
LEFT JOIN users uu ON fk.updated_by_user_id = uu.id
|
||||||
ORDER BY fk.created_at DESC
|
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
|
LEFT JOIN users uu ON fk.updated_by_user_id = uu.id
|
||||||
WHERE fk.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
|
LEFT JOIN users cu ON fk.created_by_user_id = cu.id
|
||||||
WHERE fk.serial_number = ?
|
WHERE fk.serial_number = ?
|
||||||
`);
|
`);
|
||||||
return stmt.get(serialNumber);
|
return withDecryptedPin(stmt.get(serialNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +83,7 @@ class FidoKey {
|
|||||||
WHERE fk.status = ?
|
WHERE fk.status = ?
|
||||||
ORDER BY fk.created_at DESC
|
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 = ?
|
WHERE fk.assigned_to_user_id = ?
|
||||||
ORDER BY fk.created_at DESC
|
ORDER BY fk.created_at DESC
|
||||||
`);
|
`);
|
||||||
return stmt.all(userId);
|
return stmt.all(userId).map(withDecryptedPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,9 +114,10 @@ class FidoKey {
|
|||||||
serial_number,
|
serial_number,
|
||||||
status,
|
status,
|
||||||
description,
|
description,
|
||||||
|
pin,
|
||||||
assigned_to_user_id,
|
assigned_to_user_id,
|
||||||
created_by_user_id
|
created_by_user_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const result = stmt.run(
|
const result = stmt.run(
|
||||||
@@ -118,6 +125,7 @@ class FidoKey {
|
|||||||
keyData.serial_number,
|
keyData.serial_number,
|
||||||
keyData.status,
|
keyData.status,
|
||||||
keyData.description || null,
|
keyData.description || null,
|
||||||
|
encrypt(keyData.pin) || null,
|
||||||
keyData.assigned_to_user_id || null,
|
keyData.assigned_to_user_id || null,
|
||||||
keyData.created_by_user_id
|
keyData.created_by_user_id
|
||||||
);
|
);
|
||||||
@@ -150,6 +158,10 @@ class FidoKey {
|
|||||||
fields.push('description = ?');
|
fields.push('description = ?');
|
||||||
values.push(keyData.description);
|
values.push(keyData.description);
|
||||||
}
|
}
|
||||||
|
if (keyData.pin !== undefined) {
|
||||||
|
fields.push('pin = ?');
|
||||||
|
values.push(encrypt(keyData.pin));
|
||||||
|
}
|
||||||
if (keyData.assigned_to_user_id !== undefined) {
|
if (keyData.assigned_to_user_id !== undefined) {
|
||||||
fields.push('assigned_to_user_id = ?');
|
fields.push('assigned_to_user_id = ?');
|
||||||
values.push(keyData.assigned_to_user_id);
|
values.push(keyData.assigned_to_user_id);
|
||||||
|
|||||||
@@ -38,9 +38,14 @@ class OffboardingProtocol {
|
|||||||
e.first_name as employee_first_name,
|
e.first_name as employee_first_name,
|
||||||
e.last_name as employee_last_name,
|
e.last_name as employee_last_name,
|
||||||
e.email as employee_email,
|
e.email as employee_email,
|
||||||
|
e.department as employee_department,
|
||||||
r.name as employee_role_name,
|
r.name as employee_role_name,
|
||||||
cu.username as created_by_username,
|
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
|
FROM offboarding_protocols op
|
||||||
INNER JOIN users e ON op.employee_user_id = e.id
|
INNER JOIN users e ON op.employee_user_id = e.id
|
||||||
INNER JOIN roles r ON e.role_id = r.id
|
INNER JOIN roles r ON e.role_id = r.id
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const router = express.Router();
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
const { setAuthCookie } = require('../utils/authCookie');
|
||||||
|
|
||||||
const TENANT_ID = () => process.env.AZURE_TENANT_ID;
|
const TENANT_ID = () => process.env.AZURE_TENANT_ID;
|
||||||
const CLIENT_ID = () => process.env.AZURE_CLIENT_ID;
|
const CLIENT_ID = () => process.env.AZURE_CLIENT_ID;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ const { requireAdmin } = require('../middleware/roleCheck');
|
|||||||
// Agent checkin — no JWT, uses X-Agent-Key header
|
// Agent checkin — no JWT, uses X-Agent-Key header
|
||||||
router.post('/checkin', ctrl.checkin);
|
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)
|
// Schneller Announcement-Poll (leichtgewichtig, alle 15 Sek)
|
||||||
router.post('/announcements-poll', ctrl.announcementsPoll);
|
router.post('/announcements-poll', ctrl.announcementsPoll);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ const authLimiter = rateLimit({
|
|||||||
legacyHeaders: false
|
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
|
// ROUTES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -127,6 +137,7 @@ app.get('/api/health/history', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
|
app.use('/api/auth/login', loginLimiter);
|
||||||
app.use('/api/auth', authLimiter, authRoutes);
|
app.use('/api/auth', authLimiter, authRoutes);
|
||||||
app.use('/api/users', userRoutes);
|
app.use('/api/users', userRoutes);
|
||||||
app.use('/api/fido-keys', fidoKeyRoutes);
|
app.use('/api/fido-keys', fidoKeyRoutes);
|
||||||
@@ -411,6 +422,12 @@ async function startServer() {
|
|||||||
NetworkDevice.cleanupOldChecks();
|
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)
|
// Proxmox Monitoring (alle 5 Minuten)
|
||||||
if (process.env.PROXMOX_HOST && process.env.PROXMOX_TOKEN) {
|
if (process.env.PROXMOX_HOST && process.env.PROXMOX_TOKEN) {
|
||||||
const { pollProxmox } = require('./services/proxmoxService');
|
const { pollProxmox } = require('./services/proxmoxService');
|
||||||
|
|||||||
26
backend/src/services/dataRetention.service.js
Normal file
26
backend/src/services/dataRetention.service.js
Normal 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 };
|
||||||
@@ -431,9 +431,9 @@ async function sendTicketCreatedConfirmation(ticket) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
${infoCard([
|
${infoCard([
|
||||||
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
|
['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;">${ticket.title}</strong>`],
|
['Betreff', `<strong style="color:#111827;">${escHtml(ticket.title)}</strong>`],
|
||||||
['Kategorie', `<span style="color:#374151;">${ticket.category}</span>`],
|
['Kategorie', `<span style="color:#374151;">${escHtml(ticket.category)}</span>`],
|
||||||
['Priorität', priorityBadge(ticket.priority)],
|
['Priorität', priorityBadge(ticket.priority)],
|
||||||
['Status', statusBadge(ticket.status)],
|
['Status', statusBadge(ticket.status)],
|
||||||
])}
|
])}
|
||||||
@@ -494,11 +494,11 @@ async function sendTicketAssignedNotification(ticket, assignedUser) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
${infoCard([
|
${infoCard([
|
||||||
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
|
['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;">${ticket.title}</strong>`],
|
['Betreff', `<strong style="color:#111827;">${escHtml(ticket.title)}</strong>`],
|
||||||
['Kategorie', `<span style="color:#374151;">${ticket.category}</span>`],
|
['Kategorie', `<span style="color:#374151;">${escHtml(ticket.category)}</span>`],
|
||||||
['Priorität', priorityBadge(ticket.priority)],
|
['Priorität', priorityBadge(ticket.priority)],
|
||||||
['Von', `<span style="color:#374151;">${ticket.requester_name || 'Unbekannt'}${ticket.requester_email ? ` <${ticket.requester_email}>` : ''}</span>`],
|
['Von', `<span style="color:#374151;">${escHtml(ticket.requester_name || 'Unbekannt')}${ticket.requester_email ? ` <${escHtml(ticket.requester_email)}>` : ''}</span>`],
|
||||||
])}`;
|
])}`;
|
||||||
|
|
||||||
await sendMail(
|
await sendMail(
|
||||||
@@ -554,10 +554,10 @@ async function sendCommentNotification(ticket, comment) {
|
|||||||
<table cellpadding="0" cellspacing="0" border="0">
|
<table cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td bgcolor="#0d9488" style="background-color:#0d9488;border-radius:6px;width:28px;height:28px;text-align:center;vertical-align:middle;padding:0 8px;">
|
<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>
|
||||||
<td style="padding-left:10px;vertical-align:middle;">
|
<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>
|
<span style="font-size:11px;color:#9ca3af;font-family:Inter,Helvetica,Arial,sans-serif;padding-left:6px;">· IT Support</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -567,14 +567,14 @@ async function sendCommentNotification(ticket, comment) {
|
|||||||
<!-- Message body -->
|
<!-- Message body -->
|
||||||
<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;">
|
<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, '<').replace(/>/g, '>')}
|
${escHtml(comment.comment)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
${infoCard([
|
${infoCard([
|
||||||
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
|
['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;">${ticket.title}</span>`],
|
['Betreff', `<span style="color:#374151;">${escHtml(ticket.title)}</span>`],
|
||||||
['Status', statusBadge(ticket.status)],
|
['Status', statusBadge(ticket.status)],
|
||||||
])}`;
|
])}`;
|
||||||
|
|
||||||
@@ -611,19 +611,19 @@ async function sendStaffCommentNotification(ticket, comment) {
|
|||||||
const content = `
|
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>
|
<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;">
|
<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>
|
</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;">
|
<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;">
|
<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>
|
</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;">
|
<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, '<').replace(/>/g, '>')}
|
${escHtml(comment.comment)}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
</table>
|
</table>
|
||||||
${infoCard([
|
${infoCard([
|
||||||
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
|
['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;">${ticket.title}</span>`],
|
['Betreff', `<span style="color:#374151;">${escHtml(ticket.title)}</span>`],
|
||||||
['Status', statusBadge(ticket.status)],
|
['Status', statusBadge(ticket.status)],
|
||||||
])}`;
|
])}`;
|
||||||
|
|
||||||
@@ -660,10 +660,10 @@ async function sendStaffTicketCreatedNotification(ticket) {
|
|||||||
const content = `
|
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>
|
<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([
|
${infoCard([
|
||||||
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
|
['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;">${ticket.title}</strong>`],
|
['Betreff', `<strong style="color:#111827;">${escHtml(ticket.title)}</strong>`],
|
||||||
['Von', `<span style="color:#374151;">${ticket.requester_name || ''}${ticket.requester_email ? ` <${ticket.requester_email}>` : ''}</span>`],
|
['Von', `<span style="color:#374151;">${escHtml(ticket.requester_name || '')}${ticket.requester_email ? ` <${escHtml(ticket.requester_email)}>` : ''}</span>`],
|
||||||
['Kategorie', `<span style="color:#374151;">${ticket.category || '—'}</span>`],
|
['Kategorie', `<span style="color:#374151;">${escHtml(ticket.category || '—')}</span>`],
|
||||||
['Priorität', priorityBadge(ticket.priority)],
|
['Priorität', priorityBadge(ticket.priority)],
|
||||||
])}`;
|
])}`;
|
||||||
|
|
||||||
@@ -735,8 +735,8 @@ async function sendStatusChangeNotification(ticket, oldStatus, newStatus) {
|
|||||||
${statusChangeVisual}
|
${statusChangeVisual}
|
||||||
|
|
||||||
${infoCard([
|
${infoCard([
|
||||||
['Ticket-Nr.', `<strong style="font-family:'Courier New',Courier,monospace;color:#0d9488;font-size:13px;">${ticket.ticket_number}</strong>`],
|
['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;">${ticket.title}</span>`],
|
['Betreff', `<span style="color:#374151;">${escHtml(ticket.title)}</span>`],
|
||||||
['Priorität', priorityBadge(ticket.priority)],
|
['Priorität', priorityBadge(ticket.priority)],
|
||||||
])}
|
])}
|
||||||
|
|
||||||
@@ -1032,10 +1032,10 @@ async function sendEscalationEmail(ticket) {
|
|||||||
${introHtml}
|
${introHtml}
|
||||||
</p>
|
</p>
|
||||||
<table width="100%" cellpadding="12" style="background:#fff8f8;border:1px solid #fca5a5;border-radius:8px;margin:0 0 16px;">
|
<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>Ticket:</strong> ${escHtml(ticket.ticket_number)}</td></tr>
|
||||||
<tr><td><strong>Titel:</strong> ${ticket.title}</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>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>
|
<tr><td><strong>Erstellt:</strong> ${new Date(ticket.created_at + 'Z').toLocaleString('de-DE')}</td></tr>
|
||||||
</table>`;
|
</table>`;
|
||||||
|
|
||||||
|
|||||||
18
backend/src/utils/agentAuth.js
Normal file
18
backend/src/utils/agentAuth.js
Normal 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 };
|
||||||
20
backend/src/utils/authCookie.js
Normal file
20
backend/src/utils/authCookie.js
Normal 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 };
|
||||||
34
backend/src/utils/crypto.js
Normal file
34
backend/src/utils/crypto.js
Normal 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 };
|
||||||
47
backend/src/utils/ssrfGuard.js
Normal file
47
backend/src/utils/ssrfGuard.js
Normal 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 };
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { getDatabase } = require('../config/database');
|
const { getDatabase } = require('../config/database');
|
||||||
|
const { validateAgentKey } = require('../utils/agentAuth');
|
||||||
|
|
||||||
// agentId -> WebSocket
|
// agentId -> WebSocket
|
||||||
const agentSockets = new Map();
|
const agentSockets = new Map();
|
||||||
@@ -43,12 +44,12 @@ function setupWebSocketServer(httpServer) {
|
|||||||
|
|
||||||
function handleAgent(ws, url) {
|
function handleAgent(ws, url) {
|
||||||
const key = url.searchParams.get('key');
|
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');
|
ws.close(1008, 'unauthorized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hostname = url.searchParams.get('hostname');
|
|
||||||
if (!hostname) { ws.close(1008, 'hostname required'); return; }
|
|
||||||
|
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname);
|
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) {
|
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'));
|
const agentId = parseInt(url.searchParams.get('agentId'));
|
||||||
if (!agentId) { ws.close(1008, 'agentId required'); return; }
|
if (!agentId) { ws.close(1008, 'agentId required'); return; }
|
||||||
|
|
||||||
browserSockets.set(agentId, ws);
|
let authenticated = false;
|
||||||
|
const authTimer = setTimeout(() => { if (!authenticated) ws.close(1008, 'auth timeout'); }, 5000);
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
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);
|
const aws = agentSockets.get(agentId);
|
||||||
if (!aws || aws.readyState !== WebSocket.OPEN) return;
|
if (!aws || aws.readyState !== WebSocket.OPEN) return;
|
||||||
// RTC-Signaling (offer/answer/ice) → transparent weiterleiten
|
// RTC-Signaling (offer/answer/ice) → transparent weiterleiten
|
||||||
@@ -134,9 +144,9 @@ function handleBrowser(ws, url) {
|
|||||||
|
|
||||||
function handleRdpAgent(ws, url) {
|
function handleRdpAgent(ws, url) {
|
||||||
const key = url.searchParams.get('key');
|
const key = url.searchParams.get('key');
|
||||||
if (key !== process.env.AGENT_API_KEY) { ws.close(1008, 'unauthorized'); return; }
|
|
||||||
const hostname = url.searchParams.get('hostname');
|
const hostname = url.searchParams.get('hostname');
|
||||||
if (!hostname) { ws.close(1008, 'hostname required'); return; }
|
if (!hostname) { ws.close(1008, 'hostname required'); return; }
|
||||||
|
if (!validateAgentKey(key, hostname)) { ws.close(1008, 'unauthorized'); return; }
|
||||||
|
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname);
|
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) {
|
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'));
|
const agentId = parseInt(url.searchParams.get('agentId'));
|
||||||
if (!agentId) { ws.close(1008, 'agentId required'); return; }
|
if (!agentId) { ws.close(1008, 'agentId required'); return; }
|
||||||
|
|
||||||
const oldBws = rdpBrowserSockets.get(agentId);
|
let authenticated = false;
|
||||||
if (oldBws?.readyState === WebSocket.OPEN) oldBws.close();
|
const authTimer = setTimeout(() => { if (!authenticated) ws.close(1008, 'auth timeout'); }, 5000);
|
||||||
rdpBrowserSockets.set(agentId, ws);
|
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
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);
|
const aws = rdpAgentSockets.get(agentId);
|
||||||
if (aws?.readyState === WebSocket.OPEN) aws.send(data.toString());
|
if (aws?.readyState === WebSocket.OPEN) aws.send(data.toString());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
|
||||||
997
devteam.js
997
devteam.js
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
||||||
.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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
||||||
}
|
|
||||||
|
|
||||||
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('');
|
|
||||||
});
|
|
||||||
@@ -13,9 +13,15 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=5000
|
- PORT=5000
|
||||||
- JWT_SECRET=jwt-jItWbLMHTIDElss0C3tMN6NV3hq3GjEIPXp79-X8ljiMS6OMX541Xg
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- AGENT_API_KEY=itx-4CPJPTHmCfdrL9D62WacCATEvuvULXcp7ECMpSaNUjsS344F6_L4Ug
|
- AGENT_API_KEY=${AGENT_API_KEY}
|
||||||
- SCANNER_API_KEY=nsx-aa0193d719c2be765441e1dd24bf7480
|
- 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
|
- JWT_EXPIRATION=8h
|
||||||
- DATABASE_PATH=/app/data/database.sqlite
|
- DATABASE_PATH=/app/data/database.sqlite
|
||||||
- FRONTEND_URL=https://it-nexus.cereda-systems.de
|
- FRONTEND_URL=https://it-nexus.cereda-systems.de
|
||||||
@@ -25,13 +31,13 @@ services:
|
|||||||
- SMTP_PORT=587
|
- SMTP_PORT=587
|
||||||
- SMTP_SECURE=false
|
- SMTP_SECURE=false
|
||||||
- SMTP_USER=it-tool@cereda-systems.de
|
- SMTP_USER=it-tool@cereda-systems.de
|
||||||
- SMTP_PASSWORD=Cereda2025!
|
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
- SMTP_FROM=IT Support Cereda <it-tool@cereda-systems.de>
|
- SMTP_FROM=IT Support Cereda <it-tool@cereda-systems.de>
|
||||||
# IMAP (eingehende E-Mails → Tickets)
|
# IMAP (eingehende E-Mails → Tickets)
|
||||||
- IMAP_HOST=outlook.office365.com
|
- IMAP_HOST=outlook.office365.com
|
||||||
- IMAP_PORT=993
|
- IMAP_PORT=993
|
||||||
- IMAP_USER=it-tool@cereda-systems.de
|
- IMAP_USER=it-tool@cereda-systems.de
|
||||||
- IMAP_PASSWORD=Cereda2025!
|
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
||||||
- IMAP_MAILBOX=INBOX
|
- IMAP_MAILBOX=INBOX
|
||||||
# Ticket-Links in E-Mails
|
# Ticket-Links in E-Mails
|
||||||
- TICKET_BASE_URL=https://it-nexus.cereda-systems.de
|
- TICKET_BASE_URL=https://it-nexus.cereda-systems.de
|
||||||
@@ -40,21 +46,21 @@ services:
|
|||||||
# Microsoft Azure AD (OAuth2 + Graph API)
|
# Microsoft Azure AD (OAuth2 + Graph API)
|
||||||
- AZURE_TENANT_ID=6a9a4382-ea6c-4e64-a09f-0905f83b3d64
|
- AZURE_TENANT_ID=6a9a4382-ea6c-4e64-a09f-0905f83b3d64
|
||||||
- AZURE_CLIENT_ID=de5c4a83-d24d-4323-9f3b-9c3a2f6c709f
|
- 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
|
- AZURE_REDIRECT_URI=https://it-nexus.cereda-systems.de/api/auth/microsoft/callback
|
||||||
# Anthropic Claude API (KI-Integration)
|
# Anthropic Claude API (KI-Integration)
|
||||||
- ANTHROPIC_API_KEY=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
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||||
# Teams Bot
|
# Teams Bot
|
||||||
- TEAMS_BOT_APP_ID=bd39de7e-1b85-4e0c-9301-a68cd5122523
|
- 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
|
# Microsoft Planner Sync
|
||||||
- PLANNER_PLAN_ID=CFyoAifRzUi2ONlk01smWpcAHtm3
|
- PLANNER_PLAN_ID=CFyoAifRzUi2ONlk01smWpcAHtm3
|
||||||
# Proxmox Monitoring
|
# Proxmox Monitoring
|
||||||
- PROXMOX_HOST=https://192.168.0.184:8006
|
- 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
|
- PROXMOX_NODE=hve-01
|
||||||
# GitHub API (Feedback-System)
|
# GitHub API (Feedback-System)
|
||||||
- GITHUB_TOKEN=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
|
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||||
volumes:
|
volumes:
|
||||||
# Persistente Datenbank - bleibt auch nach Container-Neustart erhalten
|
# Persistente Datenbank - bleibt auch nach Container-Neustart erhalten
|
||||||
- fido-data:/app/data
|
- fido-data:/app/data
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 |
@@ -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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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")]
|
|
||||||
@@ -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.
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
d0c739f07a28fc8609fc4a098983a9d7bd13f38d051a4df2bc76519c63de7a08
|
|
||||||
@@ -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 =
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
074cd1570727f0b97007ce35e18f8b37cb149bef28991a72fe6b298b163c25a9
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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": []
|
|
||||||
}
|
|
||||||
12
frontend/package-lock.json
generated
12
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
|
"dompurify": "^3.4.11",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -3958,7 +3959,7 @@
|
|||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
@@ -7074,6 +7075,15 @@
|
|||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
"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": {
|
"node_modules/domutils": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
|
"dompurify": "^3.4.11",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -1234,21 +1234,16 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|||||||
let shares = [];
|
let shares = [];
|
||||||
let demoMode = false;
|
let demoMode = false;
|
||||||
|
|
||||||
/* ---------- Auth token ---------- */
|
/* ---------- Auth (httpOnly-Cookie, kein Token in localStorage/URL) ---------- */
|
||||||
const TOKEN_KEY = "token";
|
let demoModeFlag = false;
|
||||||
const getToken = () => localStorage.getItem(TOKEN_KEY);
|
|
||||||
const setToken = (t) => localStorage.setItem(TOKEN_KEY, t);
|
|
||||||
const clearToken = () => localStorage.removeItem(TOKEN_KEY);
|
|
||||||
|
|
||||||
/* ---------- Fetch helper ---------- */
|
/* ---------- Fetch helper ---------- */
|
||||||
async function api(path, opts = {}) {
|
async function api(path, opts = {}) {
|
||||||
const headers = new Headers(opts.headers || {});
|
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")) {
|
if (opts.body && !(opts.body instanceof FormData) && !headers.has("Content-Type")) {
|
||||||
headers.set("Content-Type", "application/json");
|
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) {
|
if (!res.ok) {
|
||||||
const err = new Error("HTTP " + res.status);
|
const err = new Error("HTTP " + res.status);
|
||||||
err.status = 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();
|
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 ---------- */
|
/* ---------- Theme toggle ---------- */
|
||||||
(function initTheme() {
|
(function initTheme() {
|
||||||
const KEY = "cereda-theme";
|
const KEY = "cereda-theme";
|
||||||
@@ -1290,17 +1272,12 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|||||||
|
|
||||||
/* ---------- Init ---------- */
|
/* ---------- Init ---------- */
|
||||||
async function init() {
|
async function init() {
|
||||||
captureUrlToken();
|
|
||||||
const tk = getToken();
|
|
||||||
if (!tk) return showLogin();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const me = await api("/api/auth/me");
|
const me = await api("/api/auth/me");
|
||||||
showApp(me);
|
showApp(me);
|
||||||
await loadShares();
|
await loadShares();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401 || e.status === 403) {
|
if (e.status === 401 || e.status === 403) {
|
||||||
clearToken();
|
|
||||||
showLogin();
|
showLogin();
|
||||||
} else {
|
} else {
|
||||||
enterDemoMode();
|
enterDemoMode();
|
||||||
@@ -1311,7 +1288,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|||||||
/* ---------- Demo fallback ---------- */
|
/* ---------- Demo fallback ---------- */
|
||||||
function enterDemoMode() {
|
function enterDemoMode() {
|
||||||
demoMode = true;
|
demoMode = true;
|
||||||
setToken("demo-token");
|
demoModeFlag = true;
|
||||||
showApp({ username: "m.schmidt", display_name: "Marco Schmidt" });
|
showApp({ username: "m.schmidt", display_name: "Marco Schmidt" });
|
||||||
demoBadge.classList.add("on");
|
demoBadge.classList.add("on");
|
||||||
shares = seedShares();
|
shares = seedShares();
|
||||||
@@ -1391,12 +1368,11 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ username, password })
|
body: JSON.stringify({ username, password })
|
||||||
});
|
});
|
||||||
if (res && res.token) {
|
if (res && res.status === "success") {
|
||||||
setToken(res.token);
|
|
||||||
const me = await api("/api/auth/me").catch(() => null);
|
const me = await api("/api/auth/me").catch(() => null);
|
||||||
showApp(me || { username });
|
showApp(me || { username });
|
||||||
await loadShares();
|
await loadShares();
|
||||||
} else throw new Error("No token in response.");
|
} else throw new Error("Login fehlgeschlagen.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status === 401 || err.status === 403) {
|
if (err.status === 401 || err.status === 403) {
|
||||||
// Real auth rejection — show the error
|
// Real auth rejection — show the error
|
||||||
@@ -1413,7 +1389,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|||||||
|
|
||||||
/* ---------- Logout ---------- */
|
/* ---------- Logout ---------- */
|
||||||
logoutBtn.addEventListener("click", () => {
|
logoutBtn.addEventListener("click", () => {
|
||||||
clearToken();
|
if (!demoModeFlag) { api("/api/auth/logout", { method: "POST" }).catch(() => {}); }
|
||||||
shares = [];
|
shares = [];
|
||||||
pickedFile = null;
|
pickedFile = null;
|
||||||
demoMode = false;
|
demoMode = false;
|
||||||
@@ -1593,7 +1569,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|||||||
const list = await api("/api/shares");
|
const list = await api("/api/shares");
|
||||||
shares = Array.isArray(list) ? list : (list?.shares || []);
|
shares = Array.isArray(list) ? list : (list?.shares || []);
|
||||||
} catch (e) {
|
} 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
|
// 403 = not admin, just show empty list — user can still create shares
|
||||||
shares = [];
|
shares = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import aiService from '../../services/aiService';
|
import aiService from '../../services/aiService';
|
||||||
import { marked } from 'marked';
|
import { renderMd } from '../../utils/sanitizeMarkdown';
|
||||||
|
|
||||||
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>') };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const STAFF_ROLES = ['super_admin', 'admin', 'support', 'bearbeiter'];
|
const STAFF_ROLES = ['super_admin', 'admin', 'support', 'bearbeiter'];
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
|
|||||||
const connectedRef = useRef(false);
|
const connectedRef = useRef(false);
|
||||||
|
|
||||||
const getWsUrl = () => {
|
const getWsUrl = () => {
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
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) => {
|
const connect = (screen = screenIdx) => {
|
||||||
@@ -33,6 +32,7 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
|
|||||||
|
|
||||||
connectedRef.current = false;
|
connectedRef.current = false;
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
|
ws.send(JSON.stringify({ type: 'auth', token: localStorage.getItem('token') }));
|
||||||
ws.send(JSON.stringify({ type: 'rdp_start', screen }));
|
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;
|
img.src = 'data:image/jpeg;base64,' + msg.data;
|
||||||
} else if (msg.type === 'rdp_denied') {
|
} else if (msg.type === 'rdp_denied') {
|
||||||
setStatus('error');
|
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();
|
ws.close();
|
||||||
} else if (msg.type === 'rdp_disconnected') {
|
} else if (msg.type === 'rdp_disconnected') {
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
@@ -119,8 +121,14 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
|
|||||||
{/* Screen-Selektor */}
|
{/* Screen-Selektor */}
|
||||||
<select
|
<select
|
||||||
value={screenIdx}
|
value={screenIdx}
|
||||||
onChange={e => setScreenIdx(Number(e.target.value))}
|
onChange={e => {
|
||||||
disabled={status === 'connected' || status === 'connecting' || status === 'waiting'}
|
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 }}
|
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 => (
|
{SCREEN_OPTIONS.map(o => (
|
||||||
|
|||||||
@@ -177,10 +177,9 @@ function RemoteShell({ agentId, agentHostname }) {
|
|||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const getWsUrl = () => {
|
const getWsUrl = () => {
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
const host = window.location.host;
|
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 = () => {
|
const connect = () => {
|
||||||
@@ -191,7 +190,10 @@ function RemoteShell({ agentId, agentHostname }) {
|
|||||||
const ws = new WebSocket(getWsUrl());
|
const ws = new WebSocket(getWsUrl());
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.onopen = () => setStatus('connected');
|
ws.onopen = () => {
|
||||||
|
ws.send(JSON.stringify({ type: 'auth', token: localStorage.getItem('token') }));
|
||||||
|
setStatus('connected');
|
||||||
|
};
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
setOutput(prev => prev + stripAnsi(e.data));
|
setOutput(prev => prev + stripAnsi(e.data));
|
||||||
@@ -766,7 +768,7 @@ export default function AgentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── REMOTE TOOLS ─────────────────────────────────────────────────── */}
|
{/* ── REMOTE TOOLS ─────────────────────────────────────────────────── */}
|
||||||
{(isSuperAdmin || isAdmin) && (
|
{(isSuperAdmin() || isAdmin()) && (
|
||||||
<div style={{ marginTop: 24 }}>
|
<div style={{ marginTop: 24 }}>
|
||||||
<div style={{ display: 'flex', gap: 4, marginBottom: 0, borderBottom: '1px solid var(--border-color)' }}>
|
<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} />}
|
{shellTab === 'rdp' && <RemoteDesktop agentId={agent.id} agentHostname={agent.hostname} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isSuperAdmin && !isAdmin && (
|
|
||||||
<RemoteShell agentId={agent.id} agentHostname={agent.hostname} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── ANKÜNDIGUNG MODAL ────────────────────────────────────────────── */}
|
{/* ── ANKÜNDIGUNG MODAL ────────────────────────────────────────────── */}
|
||||||
{showAnnModal && (
|
{showAnnModal && (
|
||||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.6)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.6)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import aiService from '../services/aiService';
|
import aiService from '../services/aiService';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Marked } from 'marked';
|
import { renderMd } from '../utils/sanitizeMarkdown';
|
||||||
|
|
||||||
const marked = new Marked({ breaks: true, gfm: true });
|
|
||||||
const renderMd = (text) => ({ __html: marked.parse(text) });
|
|
||||||
|
|
||||||
const WELCOME_MSG = {
|
const WELCOME_MSG = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user