using LiteDB; using LehrerApp.Sync.Models; namespace LehrerApp.Api; /// /// Server-seitiger Snapshot-Speicher. /// Speichert: /// - EncryptedPayload: mit Sync-Schlüssel verschlüsselte LiteDB /// - EncryptedSyncKey: mit Code-Key (PBKDF2) verschlüsselter Sync-Schlüssel /// /// Der Server versteht beides nicht – er reicht es nur weiter. /// TTL: 24h, Einmal-Verwendung. /// public class SnapshotStore : IDisposable { private readonly LiteDatabase _db; private readonly ILiteCollection _col; private readonly Timer _cleanupTimer; private static readonly string[] Animals = [ "TIGER", "ADLER", "DACHS", "LUCHS", "FALKE", "IGEL", "ELCH", "FUCHS", "RABE", "WOLF", "BISON", "LAMM", "EULE", "BIBER", "STORCH", "HIRSCH","OTTER", "MARDER","KRANICH","LACHS", ]; private static readonly string[] Colors = [ "BLAU", "GRUEN", "ROT", "GOLD", "GRAU", "CYAN", "ROSA", "LILA", "SAND", "MINT", "SMARAGD","KORALLE","INDIGO","AMBER","JADE", ]; public SnapshotStore(string dataPath) { Directory.CreateDirectory(dataPath); _db = new LiteDatabase(Path.Combine(dataPath, "snapshots.db")); _col = _db.GetCollection("snapshots"); _col.EnsureIndex(x => x.Code); _col.EnsureIndex(x => x.ExpiresAt); _cleanupTimer = new Timer( _ => Cleanup(), null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); } // ── Upload ──────────────────────────────────────────────────────────────── /// /// Speichert Snapshot und verschlüsselten Sync-Schlüssel. /// Wenn für denselben User bereits ein Snapshot existiert, wird er ersetzt. /// public SnapshotUploadResponse Store( string userId, SnapshotUploadRequest request) { // Alten Snapshot desselben Users überschreiben _col.DeleteMany(e => e.UserId == userId); var code = GenerateCode(); var entry = new SnapshotEntry { Code = code, UserId = userId, EncryptedPayload = request.EncryptedPayload, EncryptedSyncKey = request.EncryptedSyncKey, SourceDeviceType = request.DeviceType, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddHours(24), }; _col.Insert(entry); return new SnapshotUploadResponse { Code = code, ExpiresAt = entry.ExpiresAt, }; } // ── Download (Einmal-Verwendung) ────────────────────────────────────────── public SnapshotDownloadResponse? Retrieve(string userId, string code) { var entry = _col.FindOne(e => e.UserId == userId && e.Code == code.ToUpperInvariant()); if (entry is null) return null; if (entry.ExpiresAt < DateTime.UtcNow) { _col.Delete(entry.Id); return null; } // Einmal-Verwendung: sofort löschen _col.Delete(entry.Id); return new SnapshotDownloadResponse { EncryptedPayload = entry.EncryptedPayload, EncryptedSyncKey = entry.EncryptedSyncKey, CreatedAt = entry.CreatedAt, SourceDeviceType = entry.SourceDeviceType, }; } // ── Bereinigung ─────────────────────────────────────────────────────────── private void Cleanup() { var now = DateTime.UtcNow; _col.DeleteMany(e => e.ExpiresAt < now); } // ── Code-Generierung ────────────────────────────────────────────────────── private static string GenerateCode() { var rng = Random.Shared; var animal = Animals[rng.Next(Animals.Length)]; var number = rng.Next(10, 99); var color = Colors[rng.Next(Colors.Length)]; return $"{animal}-{number}-{color}"; } public void Dispose() { _cleanupTimer.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; } = ""; /// /// Sync-Schlüssel verschlüsselt mit PBKDF2(Code). /// Leer wenn noch nicht gesetzt (zweistufiger Upload). /// public string EncryptedSyncKey { get; set; } = ""; public DeviceType SourceDeviceType { get; set; } public DateTime CreatedAt { get; set; } public DateTime ExpiresAt { get; set; } }