Nach einem Login mit korrigierter Groß-/Kleinschreibung (siehe vorheriger Fix zur userId- Stabilität) bezog sich die lokale Versionsverfolgung je Entität (BasedOnServerSeq-Cache) und der Pull-Cursor weiterhin auf das alte Konto - ServerSeq-Werte sind aber nur innerhalb des Event-Logs EINES Kontos gültig. Der Push wurde zu Recht abgelehnt, der Server kannte die Entität unter der neuen userId aber gar nicht (404 beim Nachladen), und HandleRejectedAsync gab bei einem 404 bisher einfach auf, ohne den veralteten Cache-Eintrag zu bereinigen - derselbe Fehlschlag bei jedem weiteren Sync-Versuch. Dreiteiliger Fix: (1) ein 404 beim Nachladen löscht jetzt den stale Cache-Eintrag, sodass der nächste Push die Entität korrekt als neu behandelt und selbstheilend durchgeht; (2) der "Vollständigen Sync erzwingen"-Button setzt jetzt auch die Push-Versionsverfolgung zurück, nicht nur den Pull-Cursor; (3) SyncLogin erkennt einen echten Kontowechsel künftig proaktiv anhand der kanonischen userId aus der Server-Antwort und resettet automatisch, bevor der Folgefehler überhaupt auftreten kann. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
89 lines
3.1 KiB
C#
89 lines
3.1 KiB
C#
using LehrerApp.Sync.Crypto;
|
|
using System.Text.Json;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
internal class SyncSettingsConfig
|
|
{
|
|
public string ServerUrl { get; set; } = "";
|
|
public string Username { get; set; } = "";
|
|
public string? EncryptedToken { get; set; }
|
|
/// <summary>Kanonische userId aus der letzten erfolgreichen Login-Antwort (siehe
|
|
/// SettingsViewModel.SyncLogin) — dient dem Erkennen eines Kontowechsels, unabhängig von der
|
|
/// beim Login eingegebenen Groß-/Kleinschreibung des Benutzernamens (siehe TODO 10.3.5).</summary>
|
|
public string? LastUserId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ersetzt die früheren Klartext-Helfer (<c>AppBootstrapper.LoadServerUrl</c>/<c>SaveServerUrl</c>,
|
|
/// unverschlüsselte <c>auth.token</c>-Datei). Token liegt wie beim KI-Backend-Token (siehe
|
|
/// <see cref="AiSettingsService"/>) AES-256-verschlüsselt über <see cref="SyncCrypto"/>, mit
|
|
/// eigenem, rein lokalem Schlüssel — nicht zu verwechseln mit dem in <c>AppBootstrapper</c>
|
|
/// separat verwalteten Sync-Schlüssel (<c>sync.key</c>), der die Ereignis-Payloads zwischen den
|
|
/// gepaarten Geräten verschlüsselt.
|
|
/// </summary>
|
|
public class SyncSettingsService
|
|
{
|
|
private readonly string _configPath;
|
|
private readonly string _keyPath;
|
|
private readonly byte[] _tokenKey;
|
|
private SyncSettingsConfig _config;
|
|
|
|
public string ServerUrl => _config.ServerUrl;
|
|
public string Username => _config.Username;
|
|
public string? LastUserId => _config.LastUserId;
|
|
public bool IsLoggedIn => _config.EncryptedToken is not null;
|
|
|
|
public SyncSettingsService(string appDataPath)
|
|
{
|
|
_configPath = Path.Combine(appDataPath, "sync-settings.json");
|
|
_keyPath = Path.Combine(appDataPath, "sync-token.key");
|
|
_tokenKey = SyncCrypto.LoadKey(_keyPath) ?? GenerateAndSaveKey();
|
|
_config = Load();
|
|
}
|
|
|
|
public void SetServerUrl(string url)
|
|
{
|
|
_config.ServerUrl = url.Trim();
|
|
Save();
|
|
}
|
|
|
|
public void SetCredentialsAndToken(string username, string token, string? userId = null)
|
|
{
|
|
_config.Username = username;
|
|
_config.EncryptedToken = SyncCrypto.EncryptObject(token, _tokenKey);
|
|
if (userId is not null) _config.LastUserId = userId;
|
|
Save();
|
|
}
|
|
|
|
public string? GetToken() =>
|
|
_config.EncryptedToken is null ? null : SyncCrypto.DecryptObject<string>(_config.EncryptedToken, _tokenKey);
|
|
|
|
public void Logout()
|
|
{
|
|
_config.EncryptedToken = null;
|
|
Save();
|
|
}
|
|
|
|
private byte[] GenerateAndSaveKey()
|
|
{
|
|
var key = SyncCrypto.GenerateKey();
|
|
SyncCrypto.SaveKey(key, _keyPath);
|
|
return key;
|
|
}
|
|
|
|
private void Save() => File.WriteAllText(_configPath, JsonSerializer.Serialize(_config));
|
|
|
|
private SyncSettingsConfig Load()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(_configPath))
|
|
return JsonSerializer.Deserialize<SyncSettingsConfig>(File.ReadAllText(_configPath))
|
|
?? new SyncSettingsConfig();
|
|
}
|
|
catch { /* beschädigte Konfiguration -> Standardwert */ }
|
|
return new SyncSettingsConfig();
|
|
}
|
|
}
|