156 lines
5.1 KiB
C#
156 lines
5.1 KiB
C#
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; }
|
||
}
|