Files
LehrerApp/LehrerApp.Api/SnapshotStore.cs
2026-03-29 23:47:31 +02:00

156 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using LiteDB;
using LehrerApp.Sync.Models;
namespace LehrerApp.Api;
/// <summary>
/// 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.
/// </summary>
public class SnapshotStore : IDisposable
{
private readonly LiteDatabase _db;
private readonly ILiteCollection<SnapshotEntry> _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<SnapshotEntry>("snapshots");
_col.EnsureIndex(x => x.Code);
_col.EnsureIndex(x => x.ExpiresAt);
_cleanupTimer = new Timer(
_ => Cleanup(),
null,
TimeSpan.FromHours(1),
TimeSpan.FromHours(1));
}
// ── Upload ────────────────────────────────────────────────────────────────
/// <summary>
/// Speichert Snapshot und verschlüsselten Sync-Schlüssel.
/// Wenn für denselben User bereits ein Snapshot existiert, wird er ersetzt.
/// </summary>
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; } = "";
/// <summary>
/// Sync-Schlüssel verschlüsselt mit PBKDF2(Code).
/// Leer wenn noch nicht gesetzt (zweistufiger Upload).
/// </summary>
public string EncryptedSyncKey { get; set; } = "";
public DeviceType SourceDeviceType { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime ExpiresAt { get; set; }
}