using LiteDB; using LehrerApp.Sync.Models; namespace LehrerApp.Api; /// /// Server-seitiger Event-Speicher. /// Speichert verschlüsselte Events pro User – versteht den Payload nicht. /// Eine LiteDB-Datei pro User in dataPath/{userId}.db /// public class EventStore { private readonly string _dataPath; private readonly Dictionary _dbs = new(); private readonly Lock _lock = new(); public EventStore(string dataPath) { _dataPath = dataPath; Directory.CreateDirectory(dataPath); } // ── Push: Events vom Client entgegennehmen ──────────────────────────────── public PushResponse Push(string userId, List events) { var db = GetDb(userId); var col = db.GetCollection("events"); col.EnsureIndex(x => x.ServerSequenceNr); var conflictIds = new List(); long serverSeq = GetLastSequenceNr(col); foreach (var evt in events.OrderBy(e => e.Timestamp)) { // Konflikt: anderes Gerät hat dieselbe Entity kürzlich geändert var recent = col.FindOne(e => e.EntityType == evt.EntityType && e.EntityId == evt.EntityId && e.DeviceId != evt.DeviceId && e.Timestamp > evt.Timestamp.AddSeconds(-30)); if (recent is not null) { conflictIds.Add(evt.EventId); continue; } col.Insert(new ServerEvent { EventId = evt.EventId, DeviceId = evt.DeviceId, DeviceType = evt.DeviceType, Timestamp = evt.Timestamp, ClientSequenceNr = evt.SequenceNr, ServerSequenceNr = ++serverSeq, EntityType = evt.EntityType, EntityId = evt.EntityId, Operation = evt.Operation, Payload = evt.Payload, // verschlüsselt – Server liest nicht }); } return new PushResponse { Success = true, ServerSequenceNr = serverSeq, ConflictingEventIds = conflictIds, }; } // ── Pull: neue Events für Client bereitstellen ──────────────────────────── public PullResponse Pull(string userId, long since, string requestingDeviceId) { var db = GetDb(userId); var col = db.GetCollection("events"); // Nur Events anderer Geräte zurückgeben var events = col .Find(e => e.ServerSequenceNr > since && e.DeviceId != requestingDeviceId) .OrderBy(e => e.ServerSequenceNr) .Take(500) .Select(e => new SyncEvent { EventId = e.EventId, DeviceId = e.DeviceId, DeviceType = e.DeviceType, Timestamp = e.Timestamp, SequenceNr = e.ServerSequenceNr, EntityType = e.EntityType, EntityId = e.EntityId, Operation = e.Operation, Payload = e.Payload, }) .ToList(); return new PullResponse { Events = events, ServerSequenceNr = GetLastSequenceNr(col), }; } // ── Hilfsmethoden ───────────────────────────────────────────────────────── private LiteDatabase GetDb(string userId) { lock (_lock) { if (!_dbs.TryGetValue(userId, out var db)) { // Sanitize userId für Dateinamen var safeName = string.Concat(userId .Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_')); var path = Path.Combine(_dataPath, $"{safeName}.db"); db = new LiteDatabase(path); _dbs[userId] = db; } return db; } } private static long GetLastSequenceNr(ILiteCollection col) { var last = col.FindOne(Query.All(nameof(ServerEvent.ServerSequenceNr), Query.Descending)); return last?.ServerSequenceNr ?? 0; } } // Internes Dokument im Server-EventStore internal class ServerEvent { public Guid EventId { get; set; } public string DeviceId { get; set; } = ""; public DeviceType DeviceType { get; set; } public DateTime Timestamp { get; set; } public long ClientSequenceNr { get; set; } public long ServerSequenceNr { get; set; } public string EntityType { get; set; } = ""; public string EntityId { get; set; } = ""; public string Operation { get; set; } = ""; public string Payload { get; set; } = ""; // AES-256 – Server liest nicht }