215 lines
6.9 KiB
C#
215 lines
6.9 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|