feat: sync guard - Blockieren alter Clients beim sync

This commit is contained in:
2026-08-20 23:54:04 +02:00
parent 7b883555bf
commit 59bd8abb45
19 changed files with 369 additions and 26 deletions
@@ -0,0 +1,44 @@
using System.Text.Json;
using LehrerApp.Sync;
namespace LehrerApp.Api;
/// <summary>
/// Stoppt veraltete Sync-Clients vor Authentifizierung und insbesondere vor jedem Store-Zugriff.
/// Die Prüfung gilt auch für Anhänge, Plain-Sync und Device-Pairing-Snapshots.
/// </summary>
public sealed class SyncProtocolVersionMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
if (!IsProtectedSyncPath(context.Request.Path))
{
await next(context);
return;
}
context.Response.Headers[SyncProtocol.VersionHeaderName] = SyncProtocol.CurrentVersion;
var clientVersion = context.Request.Headers[SyncProtocol.VersionHeaderName].ToString();
if (string.Equals(clientVersion, SyncProtocol.CurrentVersion, StringComparison.Ordinal))
{
await next(context);
return;
}
context.Response.StatusCode = StatusCodes.Status426UpgradeRequired;
context.Response.ContentType = "application/problem+json";
await JsonSerializer.SerializeAsync(context.Response.Body, new
{
type = "https://lehrerapp.local/problems/sync-version-mismatch",
title = "Inkompatible Sync-Version",
status = StatusCodes.Status426UpgradeRequired,
detail = $"Der Server erwartet Sync-Version {SyncProtocol.CurrentVersion}.",
expectedVersion = SyncProtocol.CurrentVersion,
receivedVersion = string.IsNullOrEmpty(clientVersion) ? null : clientVersion,
});
}
private static bool IsProtectedSyncPath(PathString path) =>
path.StartsWithSegments("/api/sync", StringComparison.OrdinalIgnoreCase) ||
path.StartsWithSegments("/api/snapshot", StringComparison.OrdinalIgnoreCase);
}