using LiteDB; using LehrerApp.Sync.Models; namespace LehrerApp.Api; public class SnapshotStore(string dataPath) : IDisposable { private readonly LiteDatabase _db = new(Path.Combine(dataPath, "snapshots.db")); private readonly Timer _cleanup; public SnapshotStore(string dataPath, bool unused = false) : this(dataPath) { Directory.CreateDirectory(dataPath); _cleanup = new(_ => Clean(), null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); } private ILiteCollection Col => _db.GetCollection("snap"); public SnapshotUploadResponse Store(string userId, SnapshotUploadRequest req) { // Zweiter Upload-Schritt (Schlüssel nachreichen, mit dem im ersten Schritt vergebenen Code // verschlüsselt): denselben Eintrag aktualisieren statt einen neuen mit neuem Code // anzulegen — sonst würde der Schlüssel dauerhaft mit dem falschen Code verknüpft bleiben // und die Entschlüsselung auf dem Empfängergerät schlägt fehl (Bug, siehe TODO.md 10.3.1). if (!string.IsNullOrEmpty(req.Code)) { var existing = Col.FindOne(e => e.UserId == userId && e.Code == req.Code.ToUpperInvariant()); if (existing is not null) { existing.EncryptedPayload = req.EncryptedPayload; existing.EncryptedSyncKey = req.EncryptedSyncKey; Col.Update(existing); return new() { Code = existing.Code, ExpiresAt = existing.ExpiresAt }; } } Col.DeleteMany(e => e.UserId == userId); var code = NewCode(); var entry = new SnapshotEntry { Code = code, UserId = userId, EncryptedPayload = req.EncryptedPayload, EncryptedSyncKey = req.EncryptedSyncKey, SourceDeviceType = req.DeviceType, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddHours(24) }; Col.Insert(entry); return new() { Code = code, ExpiresAt = entry.ExpiresAt }; } public SnapshotDownloadResponse? Retrieve(string userId, string code) { var e = Col.FindOne(x => x.UserId == userId && x.Code == code.ToUpperInvariant()); if (e is null || e.ExpiresAt < DateTime.UtcNow) { if (e is not null) Col.Delete(e.Id); return null; } Col.Delete(e.Id); return new() { EncryptedPayload = e.EncryptedPayload, EncryptedSyncKey = e.EncryptedSyncKey, CreatedAt = e.CreatedAt, SourceDeviceType = e.SourceDeviceType }; } private void Clean() => Col.DeleteMany(e => e.ExpiresAt < DateTime.UtcNow); private static string NewCode() { string[] animals = ["TIGER","ADLER","DACHS","LUCHS","FALKE","IGEL","ELCH","FUCHS","RABE","WOLF","BISON","LAMM","EULE","BIBER","STORCH"]; string[] colors = ["BLAU","GRUEN","ROT","GOLD","GRAU","CYAN","ROSA","LILA","SAND","MINT"]; return $"{animals[Random.Shared.Next(animals.Length)]}-{Random.Shared.Next(10,99)}-{colors[Random.Shared.Next(colors.Length)]}"; } public void Dispose() { _cleanup?.Dispose(); _db.Dispose(); } } internal class SnapshotEntry { public ObjectId Id { get; set; } = ObjectId.NewObjectId(); public string Code { get; set; } = ""; public string UserId { get; set; } = ""; public string EncryptedPayload { get; set; } = ""; public string EncryptedSyncKey { get; set; } = ""; public DeviceType SourceDeviceType { get; set; } public DateTime CreatedAt { get; set; } public DateTime ExpiresAt { get; set; } }