Initial
This commit is contained in:
155
LehrerApp.Api/SnapshotStore.cs
Normal file
155
LehrerApp.Api/SnapshotStore.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user