using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Data; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Sync; using LehrerApp.Sync.Crypto; using System.Collections.ObjectModel; using System.Globalization; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Serialization; namespace LehrerApp.Desktop.ViewModels.Settings; public partial class SettingsViewModel { // ── Geräte-Pairing (10.3.1: Schlüsselübertragung auf ein zweites Gerät) ─── // // SnapshotService ist nur registriert, wenn bereits eine Server-URL konfiguriert ist (siehe // AppBootstrapper) — daher optional/nullable statt eines Pflicht-Konstruktorparameters. [ObservableProperty] private string _pairingCode = ""; [ObservableProperty] private string _pairingCodeInput = ""; [ObservableProperty] private string _pairingStatus = ""; [ObservableProperty] private bool _pairingBusy; /// Vom Code-Behind gesetzt: zeigt einen Bestätigungsdialog, bevor die lokale Datenbank durch /// den Stand des anderen Geräts ersetzt wird. public Func>? OnConfirmPairingRestore { get; set; } /// Vom Code-Behind gesetzt: zeigt einen Fehlerdialog, den der Nutzer aktiv wegklicken muss — /// notwendig, weil RedeemPairingCode() nach einem Fehlschlag sofort neu startet (_dbContext ist /// zu dem Zeitpunkt bereits disposed, siehe dort) und PairingStatus dadurch sonst nie sichtbar /// gerendert würde. public Func? OnShowPairingError { get; set; } // ── Geräte-Pairing: Code erzeugen / einlösen ───────────────────────────── // // CreateAndUploadAsync lädt einen verschlüsselten Snapshot der lokalen Datenbank samt // Sync-Schlüssel hoch, RestoreFromCodeAsync ersetzt auf dem ZWEITEN Gerät die dortige // Datenbank vollständig durch diesen Snapshot (vorheriger Stand wird automatisch als // .backup-Datei gesichert, siehe SnapshotService) — deshalb vor dem Einlösen ein // Bestätigungsdialog wie bei RestoreBackup. [RelayCommand] private async Task CreatePairingCode() { if (_snapshotService is null) return; PairingBusy = true; PairingCode = ""; PairingStatus = ""; void OnProgress(SnapshotProgress p) => PairingStatus = p.Message; _snapshotService.ProgressChanged += OnProgress; try { var result = await _snapshotService.CreateAndUploadAsync(); PairingCode = result.Code; PairingStatus = $"Gültig bis {result.ExpiresAt:dd.MM.yyyy HH:mm} — auf dem anderen Gerät eingeben."; } catch (Exception ex) when (ex is HttpRequestException or InvalidOperationException) { PairingStatus = $"Fehlgeschlagen: {ex.Message}"; } finally { _snapshotService.ProgressChanged -= OnProgress; PairingBusy = false; } } [RelayCommand] private async Task RedeemPairingCode() { if (_snapshotService is null) return; if (string.IsNullOrWhiteSpace(PairingCodeInput)) { PairingStatus = "Bitte Code eingeben."; return; } if (OnConfirmPairingRestore is not null && !await OnConfirmPairingRestore()) return; PairingBusy = true; PairingStatus = ""; void OnProgress(SnapshotProgress p) => PairingStatus = p.Message; _snapshotService.ProgressChanged += OnProgress; // LiteDB hält beim Öffnen einen exklusiven Dateilock — muss vor dem Überschreiben // geschlossen sein. Läuft danach ein Fehler (falscher Code, Server nicht erreichbar), ist // _dbContext bereits disposed und die App nicht mehr sicher weiter benutzbar, ohne dass // die Datenbankdatei selbst angefasst wurde (RestoreFromCodeAsync schreibt sie erst ganz // am Ende) — deshalb in JEDEM Fall (Erfolg wie Fehlschlag) neu starten, nicht nur bei // Erfolg. Ein Neustart öffnet dann wieder dieselbe, unveränderte Datenbank. _dbContext.Dispose(); try { await _snapshotService.RestoreFromCodeAsync(PairingCodeInput.Trim(), AppBootstrapper.DbPath); } catch (Exception ex) when (ex is SnapshotNotFoundException or InvalidOperationException or HttpRequestException) { // _dbContext ist bereits disposed, PairingStatus ist aber ein reines ViewModel-Feld // ohne DB-Zugriff - das Setzen ist unabhängig davon noch sicher. PairingStatus = $"Fehlgeschlagen: {ex.Message}"; _logger.Error("Geräte-Pairing (Redeem) fehlgeschlagen", ex); // Ohne diesen Dialog würde PairingStatus nie gerendert: der direkt folgende Neustart // (unten, auch im Fehlerfall nötig, siehe Kommentar oben) beendet den Prozess, bevor // Avalonia den nächsten Frame zeichnen könnte. if (OnShowPairingError is not null) await OnShowPairingError(ex.Message); } AppBootstrapper.RestartApplication(); } }