Files
LehrerApp/LehrerApp.Desktop/AppBootstrapper.cs
T
adminandClaude Sonnet 5 48d07a2b99 Datensicherheit: Backup, Verschlüsselung, App-Sperre (Kapitel 13.3)
Automatisches rollierendes Backup der Datenbank beim Start mit
Wiederherstellung über die Einstellungen, versionierte Schema-Migration,
optionale Passwort-Verschlüsselung der LiteDB-Datei und eine App-Sperre
nach Inaktivität mit eigenem Passwort.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 13:07:36 +02:00

217 lines
10 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 LehrerApp.Core.Interfaces;
using LehrerApp.Core.Services;
using LehrerApp.Data;
using LehrerApp.Data.Repositories;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Settings;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Sync;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop;
/// <summary>
/// Konfiguriert den DI-Container für den Desktop-Client.
///
/// WICHTIG: Alle Repositories sind Singleton eine LiteDB-Datei pro Nutzer.
/// Sync-Services werden nur registriert wenn eine Server-URL konfiguriert ist.
/// </summary>
public static class AppBootstrapper
{
public static string DbPath { get; private set; } = "";
public static string AppDataPath { get; private set; } = "";
/// <summary>
/// Vor <see cref="BuildServices"/> gesetzt, wenn die Datenbank passwortgeschützt ist
/// (siehe App.axaml.cs: Passwort-Abfrage vor dem Öffnen der Datenbank).
/// </summary>
public static string? DbPassword { get; set; }
/// <summary>
/// Vor der eigentlichen DI-Konfiguration verfügbar (z.B. für den globalen
/// Exception-Handler in Program.cs, der schon vor <see cref="BuildServices"/> greifen muss).
/// </summary>
public static AppLogger Logger { get; private set; } = null!;
public static string ResolveAppDataPath()
{
if (!string.IsNullOrEmpty(AppDataPath)) return AppDataPath;
var appData = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"LehrerApp");
Directory.CreateDirectory(appData);
AppDataPath = appData;
return appData;
}
/// Vor dem Öffnen der Datenbank verfügbar (z.B. für die Passwort-Abfrage beim Start).
public static string ResolveDbPath()
{
if (string.IsNullOrEmpty(DbPath))
DbPath = Path.Combine(ResolveAppDataPath(), "lehrerapp.db");
return DbPath;
}
public static AppLogger EnsureLogger()
{
Logger ??= new AppLogger(ResolveAppDataPath());
return Logger;
}
public static bool IsDbEncrypted() =>
new DatabaseEncryptionService().IsEncrypted(ResolveDbPath());
/// Beendet den Prozess und startet ihn neu (z.B. nach Wiederherstellung eines Backups
/// oder einer Änderung der Datenbank-Verschlüsselung — die laufende LiteDB-Verbindung
/// kann nicht sicher "heiß" auf eine andere Datei umgehängt werden).
public static void RestartApplication()
{
var exePath = Environment.ProcessPath;
if (!string.IsNullOrEmpty(exePath))
System.Diagnostics.Process.Start(exePath);
Environment.Exit(0);
}
public static IServiceProvider BuildServices()
{
var services = new ServiceCollection();
// ── Pfade ─────────────────────────────────────────────────────────────
var appData = ResolveAppDataPath();
ResolveDbPath();
var queuePath = Path.Combine(appData, "syncqueue.db");
var keyPath = Path.Combine(appData, "sync.key");
// ── Logging & Benachrichtigungen ─────────────────────────────────────────
EnsureLogger();
services.AddSingleton(Logger);
services.AddSingleton<NotificationService>();
// ── Datensicherheit (13.3) ───────────────────────────────────────────
// Backup läuft vor dem Öffnen der Datenbank, rein dateibasiert (unabhängig von
// Verschlüsselung) — reine Datei-Kopie, kein offener LiteDB-Handle nötig.
var backup = new BackupService(appData);
backup.CreateBackup(DbPath);
services.AddSingleton(backup);
services.AddSingleton(_ => new AppLockService(appData));
services.AddSingleton<DatabaseEncryptionService>();
// ── Datenbank ─────────────────────────────────────────────────────────
services.AddSingleton(_ => new LiteDbContext(DbPath, DbPassword));
// ── Repositories ──────────────────────────────────────────────────────
services.AddSingleton<IStudentRepository, StudentRepository>();
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<IGroupMembershipRepository, GroupMembershipRepository>();
services.AddSingleton<IExamRepository, ExamRepository>();
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
services.AddSingleton<IGradeRepository, GradeRepository>();
services.AddSingleton<IGradingSchemeRepository, GradingSchemeRepository>();
services.AddSingleton<IReportGradeRepository, ReportGradeRepository>();
services.AddSingleton<IGradingKeyTemplateRepository, GradingKeyTemplateRepository>();
services.AddSingleton<IUnitRepository, UnitRepository>();
services.AddSingleton<ILessonRepository, LessonRepository>();
services.AddSingleton<IDocumentationRepository, DocumentationRepository>();
services.AddSingleton<IWorkTaskRepository, WorkTaskRepository>();
services.AddSingleton<ITimeEntryRepository, TimeEntryRepository>();
services.AddSingleton<IParticipationSessionRepository, ParticipationSessionRepository>();
services.AddSingleton<IParticipationRepository, ParticipationRepository>();
services.AddSingleton<IParticipationAspectRepository, ParticipationAspectRepository>();
services.AddSingleton<IParticipationSectionRepository, ParticipationSectionRepository>();
services.AddSingleton<ISubjectRepository, SubjectRepository>();
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
// ── Services ──────────────────────────────────────────────────────────
services.AddSingleton<GradingService>();
services.AddSingleton<SchoolYearService>();
// ── Sync (optional nur wenn Server konfiguriert) ────────────────────
services.AddSingleton(_ => new EventQueue(queuePath));
services.AddSingleton(sp => new ConflictResolver(sp.GetRequiredService<EventQueue>()));
services.AddSingleton<byte[]>(_ =>
{
var key = SyncCrypto.LoadKey(keyPath) ?? SyncCrypto.GenerateKey();
SyncCrypto.SaveKey(key, keyPath);
return key;
});
var serverUrl = LoadServerUrl(appData);
var deviceId = LoadOrCreateDeviceId(appData);
if (!string.IsNullOrEmpty(serverUrl))
{
services.AddSingleton<SyncEngine>(sp => new SyncEngine(
sp.GetRequiredService<EventQueue>(),
sp.GetRequiredService<ConflictResolver>(),
BuildHttp(serverUrl, appData),
new SyncConfig
{
ServerUrl = serverUrl,
DeviceId = deviceId,
DeviceType = DeviceType.Desktop,
AutoSyncIntervalMinutes = 5,
}));
services.AddSingleton<SnapshotService>(sp => new SnapshotService(
BuildHttp(serverUrl, appData),
sp.GetRequiredService<LiteDbContext>(),
sp.GetRequiredService<byte[]>(),
DeviceType.Desktop, DbPath, keyPath));
}
// ── ViewModels ────────────────────────────────────────────────────────
// Singleton: einmal erstellt, überall dieselbe Instanz
services.AddSingleton<AppLockViewModel>();
services.AddSingleton<MainWindowViewModel>();
services.AddSingleton<DashboardViewModel>();
services.AddSingleton(sp =>
new SyncStatusViewModel(sp.GetService<SyncEngine>()));
services.AddSingleton<GroupListViewModel>();
services.AddSingleton<StudentListViewModel>();
// Transient: neue Instanz pro Navigation (für Detailseiten)
services.AddTransient<GroupDetailViewModel>();
services.AddTransient<StudentDetailViewModel>();
services.AddTransient<ParticipationTabViewModel>();
services.AddTransient<GradeOverviewTabViewModel>();
services.AddTransient<AddGroupDialogViewModel>();
services.AddTransient<SettingsViewModel>();
return services.BuildServiceProvider();
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private static HttpClient BuildHttp(string url, string appData)
{
var http = new HttpClient { BaseAddress = new Uri(url) };
var tokenPath = Path.Combine(appData, "auth.token");
if (File.Exists(tokenPath))
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", File.ReadAllText(tokenPath).Trim());
return http;
}
public static string LoadServerUrl(string? path = null) =>
File.Exists(Path.Combine(path ?? AppDataPath, "server.txt"))
? File.ReadAllText(Path.Combine(path ?? AppDataPath, "server.txt")).Trim()
: "";
public static void SaveServerUrl(string url) =>
File.WriteAllText(Path.Combine(AppDataPath, "server.txt"), url);
private static string LoadOrCreateDeviceId(string appData)
{
var p = Path.Combine(appData, "device.id");
if (File.Exists(p)) return File.ReadAllText(p).Trim();
var id = Guid.NewGuid().ToString();
File.WriteAllText(p, id);
return id;
}
}