feat: sync guard - Blockieren alter Clients beim sync
This commit is contained in:
@@ -253,7 +253,9 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<MainWindowViewModel>();
|
||||
services.AddSingleton<DashboardViewModel>();
|
||||
services.AddSingleton(sp =>
|
||||
new SyncStatusViewModel(sp.GetService<SyncEngine>()));
|
||||
new SyncStatusViewModel(
|
||||
sp.GetService<SyncEngine>(),
|
||||
sp.GetRequiredService<NotificationService>()));
|
||||
services.AddSingleton<GroupListViewModel>();
|
||||
services.AddSingleton<StudentListViewModel>();
|
||||
services.AddSingleton<TimetableViewModel>();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using LehrerApp.Sync;
|
||||
|
||||
namespace LehrerApp.Desktop.Services;
|
||||
|
||||
public class SyncAuthException(string userMessage) : Exception(userMessage);
|
||||
|
||||
public enum SyncConnectionTestResult { Ok, Unauthorized, Unreachable }
|
||||
public enum SyncConnectionTestResult { Ok, Unauthorized, IncompatibleVersion, Unreachable }
|
||||
|
||||
/// <summary>
|
||||
/// Login/Verbindungstest gegen den eigenen Sync-Server (LehrerApp.Api) — getrennt vom bereits
|
||||
@@ -27,7 +28,10 @@ public class SyncAuthService(HttpClient http)
|
||||
HttpResponseMessage resp;
|
||||
try
|
||||
{
|
||||
resp = await http.PostAsJsonAsync(CombineUrl(serverUrl, "/api/auth/login"), new { username, password });
|
||||
using var req = SyncProtocol.CreateRequest(HttpMethod.Post,
|
||||
CombineUrl(serverUrl, "/api/auth/login"));
|
||||
req.Content = JsonContent.Create(new { username, password });
|
||||
resp = await http.SendAsync(req);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
@@ -49,6 +53,7 @@ public class SyncAuthService(HttpClient http)
|
||||
public async Task<SyncConnectionTestResult> TestConnectionAsync(string serverUrl, string? token)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, CombineUrl(serverUrl, "/api/sync/status"));
|
||||
req.Headers.Add(SyncProtocol.VersionHeaderName, SyncProtocol.CurrentVersion);
|
||||
if (token is not null)
|
||||
req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
@@ -56,6 +61,8 @@ public class SyncAuthService(HttpClient http)
|
||||
{
|
||||
var resp = await http.SendAsync(req);
|
||||
if (resp.StatusCode == HttpStatusCode.Unauthorized) return SyncConnectionTestResult.Unauthorized;
|
||||
if (resp.StatusCode == HttpStatusCode.UpgradeRequired)
|
||||
return SyncConnectionTestResult.IncompatibleVersion;
|
||||
return resp.IsSuccessStatusCode ? SyncConnectionTestResult.Ok : SyncConnectionTestResult.Unreachable;
|
||||
}
|
||||
catch (HttpRequestException) { return SyncConnectionTestResult.Unreachable; }
|
||||
|
||||
@@ -512,6 +512,9 @@ public partial class SettingsViewModel : ObservableObject
|
||||
{
|
||||
SyncConnectionTestResult.Ok => "Verbindung erfolgreich.",
|
||||
SyncConnectionTestResult.Unauthorized => "Server erreichbar, aber nicht angemeldet oder Anmeldung abgelaufen.",
|
||||
SyncConnectionTestResult.IncompatibleVersion =>
|
||||
"Diese App-Version ist veraltet oder nicht mehr mit dem Sync-Server kompatibel. " +
|
||||
"Bitte aktualisiere die LehrerApp.",
|
||||
_ => "Server nicht erreichbar. Bitte Adresse und Internetverbindung prüfen.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Sync;
|
||||
using LehrerApp.Sync.Models;
|
||||
|
||||
@@ -8,11 +9,19 @@ namespace LehrerApp.Desktop.ViewModels;
|
||||
public partial class SyncStatusViewModel : ObservableObject
|
||||
{
|
||||
private readonly SyncEngine? _engine;
|
||||
private readonly NotificationService? _notifications;
|
||||
private bool _incompatibleNotificationShown;
|
||||
|
||||
public const string IncompatibleVersionMessage =
|
||||
"Diese App-Version ist veraltet oder nicht mit dem Sync-Server kompatibel. " +
|
||||
"Bitte aktualisiere die LehrerApp. Die Synchronisierung wurde zum Schutz deiner Daten angehalten.";
|
||||
|
||||
[ObservableProperty] private string _statusText = "Kein Server konfiguriert";
|
||||
[ObservableProperty] private string _lastSyncText = "";
|
||||
[ObservableProperty] private bool _isSyncing;
|
||||
[ObservableProperty] private bool _isServerConfigured;
|
||||
[ObservableProperty] private bool _isIncompatibleVersion;
|
||||
[ObservableProperty] private bool _canAttemptSync;
|
||||
[ObservableProperty] private int _pendingCount;
|
||||
|
||||
/// <summary>Feuert, wenn ein Sync tatsächlich Daten angewendet hat (siehe SyncEngine.
|
||||
@@ -21,10 +30,12 @@ public partial class SyncStatusViewModel : ObservableObject
|
||||
/// schreibt.</summary>
|
||||
public event Action? DataChanged;
|
||||
|
||||
public SyncStatusViewModel(SyncEngine? engine)
|
||||
public SyncStatusViewModel(SyncEngine? engine, NotificationService? notifications = null)
|
||||
{
|
||||
_engine = engine;
|
||||
_notifications = notifications;
|
||||
IsServerConfigured = engine is not null;
|
||||
CanAttemptSync = IsServerConfigured;
|
||||
if (_engine is not null)
|
||||
{
|
||||
_engine.StatusChanged += OnStatus;
|
||||
@@ -41,6 +52,8 @@ public partial class SyncStatusViewModel : ObservableObject
|
||||
private void OnStatus(SyncStatus s)
|
||||
{
|
||||
IsSyncing = s.State == SyncState.Syncing;
|
||||
IsIncompatibleVersion = s.State == SyncState.IncompatibleVersion;
|
||||
CanAttemptSync = IsServerConfigured && !IsIncompatibleVersion;
|
||||
PendingCount = s.PendingEvents;
|
||||
StatusText = s.State switch
|
||||
{
|
||||
@@ -48,9 +61,16 @@ public partial class SyncStatusViewModel : ObservableObject
|
||||
SyncState.Syncing => "Synchronisiere…",
|
||||
SyncState.Offline => "Offline",
|
||||
SyncState.Error => $"Fehler: {s.ErrorMessage}",
|
||||
SyncState.IncompatibleVersion => "Update erforderlich",
|
||||
_ => "",
|
||||
};
|
||||
LastSyncText = s.LastSyncAt.HasValue ? $"Zuletzt: {s.LastSyncAt:HH:mm}" : "Noch nie";
|
||||
|
||||
if (IsIncompatibleVersion && !_incompatibleNotificationShown)
|
||||
{
|
||||
_incompatibleNotificationShown = true;
|
||||
_notifications?.ShowError(IncompatibleVersionMessage);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSync))]
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
<DrawerPage x:Name="RootDrawer"
|
||||
DrawerLength="220"
|
||||
DrawerBehavior="Auto"
|
||||
DrawerLayoutBehavior="CompactInline"
|
||||
Content="{Binding CurrentPage}">
|
||||
DrawerLayoutBehavior="CompactInline">
|
||||
|
||||
<DrawerPage.DataTemplates>
|
||||
<DataTemplate DataType="vm:DashboardViewModel">
|
||||
@@ -63,6 +62,26 @@
|
||||
</DataTemplate>
|
||||
</DrawerPage.DataTemplates>
|
||||
|
||||
<DrawerPage.Content>
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top"
|
||||
IsVisible="{Binding SyncStatus.IsIncompatibleVersion}"
|
||||
Background="#B3261E" Padding="16,12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Text="⚠" FontSize="18" Foreground="White"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel>
|
||||
<TextBlock Text="Software-Update erforderlich"
|
||||
Foreground="White" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{x:Static vm:SyncStatusViewModel.IncompatibleVersionMessage}"
|
||||
Foreground="White" Opacity="0.92" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ContentControl Content="{Binding CurrentPage}"/>
|
||||
</DockPanel>
|
||||
</DrawerPage.Content>
|
||||
|
||||
<DrawerPage.Drawer>
|
||||
<DockPanel Classes.compact="{Binding !#RootDrawer.IsOpen}">
|
||||
<DockPanel.Styles>
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
x:Class="LehrerApp.Desktop.Views.SyncStatusBar"
|
||||
x:DataType="vm:SyncStatusViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<StackPanel Grid.Column="0" IsVisible="{Binding !IsIncompatibleVersion}">
|
||||
<TextBlock Text="{Binding StatusText}" FontSize="12" Opacity="0.7"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding LastSyncText}" FontSize="10" Opacity="0.4"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="0" IsVisible="{Binding IsIncompatibleVersion}">
|
||||
<TextBlock Text="⚠ Update erforderlich" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="#D32F2F"/>
|
||||
<TextBlock Text="Sync angehalten" FontSize="10" Foreground="#D32F2F" Opacity="0.8"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="↻" FontSize="14"
|
||||
Command="{Binding SyncNowCommand}"
|
||||
IsVisible="{Binding IsServerConfigured}"
|
||||
IsVisible="{Binding CanAttemptSync}"
|
||||
Background="Transparent" Padding="6,4"
|
||||
ToolTip.Tip="Jetzt synchronisieren"/>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user