This commit is contained in:
2026-03-29 23:47:31 +02:00
commit 216d5d2280
75 changed files with 5702 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
using System.Text;
using System.Text.Json;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
namespace LehrerApp.Sync;
/// <summary>
/// Wendet vom Server gezogene Events auf die lokale LiteDB an.
///
/// Unterscheidet zwischen:
/// - Verschlüsselten Desktop-Events → erst entschlüsseln, dann anwenden
/// - Klartext-Events von WebApp/Companion → direkt anwenden
/// </summary>
public class EventApplier
{
private readonly byte[] _key;
// Repositories für alle Entity-Typen
private readonly IStudentRepository _students;
private readonly IGroupRepository _groups;
private readonly IEnrollmentRepository _enrollments;
private readonly IExamRepository _exams;
private readonly IExamResultRepository _examResults;
private readonly IGradeRepository _grades;
private readonly IUnitRepository _units;
private readonly ILessonRepository _lessons;
private readonly IDocumentationRepository _docs;
private readonly IWorkTaskRepository _tasks;
private readonly ITimeEntryRepository _timeEntries;
public EventApplier(
byte[] key,
IStudentRepository students,
IGroupRepository groups,
IEnrollmentRepository enrollments,
IExamRepository exams,
IExamResultRepository examResults,
IGradeRepository grades,
IUnitRepository units,
ILessonRepository lessons,
IDocumentationRepository docs,
IWorkTaskRepository tasks,
ITimeEntryRepository timeEntries)
{
_key = key;
_students = students;
_groups = groups;
_enrollments = enrollments;
_exams = exams;
_examResults = examResults;
_grades = grades;
_units = units;
_lessons = lessons;
_docs = docs;
_tasks = tasks;
_timeEntries = timeEntries;
}
// ── Hauptmethode ──────────────────────────────────────────────────────────
public void Apply(SyncEvent evt)
{
var json = ResolvePayload(evt);
if (string.IsNullOrWhiteSpace(json)) return;
try
{
ApplyJson(evt.EntityType, evt.Operation, json);
}
catch (Exception ex)
{
// Einzelne fehlerhafte Events nicht die ganze Sync-Session abbrechen
Console.Error.WriteLine(
$"EventApplier: Fehler bei {evt.EntityType}/{evt.Operation} " +
$"({evt.EventId}): {ex.Message}");
}
}
public void ApplyAll(IEnumerable<SyncEvent> events)
{
// Chronologisch anwenden
foreach (var evt in events.OrderBy(e => e.Timestamp))
Apply(evt);
}
// ── Payload auflösen ──────────────────────────────────────────────────────
private string ResolvePayload(SyncEvent evt)
{
// Klartext-Events von WebApp/Companion
if (evt.DeviceType == DeviceType.Companion)
return evt.Payload;
// Verschlüsselte Events vom Desktop entschlüsseln
try
{
var encrypted = Convert.FromBase64String(evt.Payload);
var decrypted = SyncCrypto.Decrypt(encrypted, _key);
return Encoding.UTF8.GetString(decrypted);
}
catch (Exception ex)
{
Console.Error.WriteLine(
$"EventApplier: Entschlüsselung fehlgeschlagen " +
$"({evt.EventId}): {ex.Message}");
return "";
}
}
// ── Entity-spezifische Anwendung ──────────────────────────────────────────
private void ApplyJson(string entityType, string operation, string json)
{
switch (entityType)
{
case "Student":
ApplyEntity<Student>(operation, json,
e => _students.Save(e),
id => _students.Delete(id));
break;
case "LearningGroup":
ApplyEntity<LearningGroup>(operation, json,
e => _groups.Save(e),
id => _groups.Delete(id));
break;
case "Enrollment":
ApplyEntity<Enrollment>(operation, json,
e => _enrollments.Save(e),
id => _enrollments.Delete(id));
break;
case "Exam":
ApplyEntity<Exam>(operation, json,
e => _exams.Save(e),
id => _exams.Delete(id));
break;
case "ExamResult":
ApplyEntity<ExamResult>(operation, json,
e => _examResults.Save(e),
_ => { }); // ExamResults werden nicht einzeln gelöscht
break;
case "Grade":
ApplyEntity<Grade>(operation, json,
e => _grades.Save(e),
id => _grades.Delete(id));
break;
case "Unit":
ApplyEntity<Unit>(operation, json,
e => _units.Save(e),
id => _units.Delete(id));
break;
case "Lesson":
ApplyEntity<Lesson>(operation, json,
e => _lessons.Save(e),
id => _lessons.Delete(id));
break;
case "Documentation":
ApplyEntity<Documentation>(operation, json,
e => _docs.Save(e),
id => _docs.Delete(id));
break;
case "WorkTask":
ApplyEntity<WorkTask>(operation, json,
e => _tasks.Save(e),
id => _tasks.Delete(id));
break;
case "TimeEntry":
ApplyEntity<TimeEntry>(operation, json,
e => _timeEntries.Save(e),
id => _timeEntries.Delete(id));
break;
default:
Console.Error.WriteLine(
$"EventApplier: Unbekannter EntityType '{entityType}'");
break;
}
}
private static void ApplyEntity<T>(
string operation,
string json,
Action<T> save,
Action<Guid> delete) where T : class
{
switch (operation)
{
case "Upsert":
case "Create":
case "Update":
var entity = JsonSerializer.Deserialize<T>(json);
if (entity is not null) save(entity);
break;
case "Delete":
// Payload ist bei Delete nur die ID
if (Guid.TryParse(json.Trim('"'), out var id))
delete(id);
break;
}
}
}