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>
This commit is contained in:
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Data;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -18,6 +19,10 @@ public partial class SettingsViewModel : ObservableObject
|
||||
private readonly IGradingKeyTemplateRepository _gradingKeyTemplates;
|
||||
private readonly IGradingSchemeRepository _gradingSchemes;
|
||||
private readonly GradingService _grading;
|
||||
private readonly BackupService _backups;
|
||||
private readonly DatabaseEncryptionService _dbEncryption;
|
||||
private readonly AppLockService _appLock;
|
||||
private readonly LiteDbContext _dbContext;
|
||||
|
||||
// ── Fächer ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -51,20 +56,183 @@ public partial class SettingsViewModel : ObservableObject
|
||||
[ObservableProperty] private GradingSchemeEditItem _classScheme = null!;
|
||||
[ObservableProperty] private GradingSchemeEditItem _courseScheme = null!;
|
||||
|
||||
// ── Sicherheit: Datensicherung (13.3.1/13.3.2) ───────────────────────────
|
||||
|
||||
[ObservableProperty] private string _backupStatus = "";
|
||||
public ObservableCollection<BackupListItem> Backups { get; } = [];
|
||||
|
||||
/// Vom Code-Behind gesetzt: zeigt einen Bestätigungsdialog vor dem Wiederherstellen.
|
||||
public Func<BackupListItem, Task<bool>>? OnConfirmRestore { get; set; }
|
||||
|
||||
// ── Sicherheit: Datenbank-Verschlüsselung (13.3.4) ───────────────────────
|
||||
|
||||
[ObservableProperty] private bool _isDbEncrypted;
|
||||
[ObservableProperty] private string _currentDbPassword = "";
|
||||
[ObservableProperty] private string _newDbPassword = "";
|
||||
[ObservableProperty] private string _newDbPasswordConfirm = "";
|
||||
[ObservableProperty] private string _dbPasswordError = "";
|
||||
|
||||
public string DbEncryptionStatusLabel => IsDbEncrypted ? "verschlüsselt" : "nicht verschlüsselt";
|
||||
|
||||
partial void OnIsDbEncryptedChanged(bool value) => OnPropertyChanged(nameof(DbEncryptionStatusLabel));
|
||||
|
||||
// ── Sicherheit: App-Sperre nach Inaktivität (13.3.5) ─────────────────────
|
||||
|
||||
[ObservableProperty] private bool _appLockEnabled;
|
||||
[ObservableProperty] private int _appLockTimeoutMinutes = 10;
|
||||
[ObservableProperty] private string _appLockNewPassword = "";
|
||||
[ObservableProperty] private string _appLockNewPasswordConfirm = "";
|
||||
[ObservableProperty] private string _appLockPasswordError = "";
|
||||
[ObservableProperty] private string _appLockStatus = "";
|
||||
|
||||
public bool AppLockHasPassword => _appLock.HasPassword;
|
||||
|
||||
/// Vom Code-Behind gesetzt: aktualisiert den laufenden Inaktivitäts-Timer sofort,
|
||||
/// ohne dass die App neu gestartet werden muss.
|
||||
public Action? OnAppLockChanged { get; set; }
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||
GradingService grading)
|
||||
GradingService grading, BackupService backups, DatabaseEncryptionService dbEncryption,
|
||||
AppLockService appLock, LiteDbContext dbContext)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_domainRepo = domainRepo;
|
||||
_gradingKeyTemplates = gradingKeyTemplates;
|
||||
_gradingSchemes = gradingSchemes;
|
||||
_grading = grading;
|
||||
_backups = backups;
|
||||
_dbEncryption = dbEncryption;
|
||||
_appLock = appLock;
|
||||
_dbContext = dbContext;
|
||||
LoadSubjects();
|
||||
LoadGradingKeyTemplates();
|
||||
LoadGradingSchemes();
|
||||
LoadBackups();
|
||||
IsDbEncrypted = _dbEncryption.IsEncrypted(AppBootstrapper.DbPath);
|
||||
AppLockEnabled = _appLock.IsEnabled;
|
||||
AppLockTimeoutMinutes = _appLock.TimeoutMinutes;
|
||||
}
|
||||
|
||||
// ── Datensicherung: Laden / Erstellen / Wiederherstellen ─────────────────
|
||||
|
||||
private void LoadBackups()
|
||||
{
|
||||
Backups.Clear();
|
||||
foreach (var b in _backups.ListBackups()) Backups.Add(new BackupListItem(b));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateBackupNow()
|
||||
{
|
||||
_backups.CreateBackup(AppBootstrapper.DbPath);
|
||||
LoadBackups();
|
||||
BackupStatus = "Backup erstellt.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RestoreBackup(BackupListItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
if (OnConfirmRestore is not null && !await OnConfirmRestore(item)) return;
|
||||
|
||||
_dbContext.Dispose();
|
||||
_backups.RestoreBackup(item.Path, AppBootstrapper.DbPath);
|
||||
AppBootstrapper.RestartApplication();
|
||||
}
|
||||
|
||||
// ── Datenbank-Verschlüsselung: Setzen / Ändern / Entfernen ───────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveDbPassword()
|
||||
{
|
||||
DbPasswordError = "";
|
||||
var valid = true;
|
||||
var wantsPassword = !string.IsNullOrWhiteSpace(NewDbPassword) || !string.IsNullOrWhiteSpace(NewDbPasswordConfirm);
|
||||
|
||||
if (IsDbEncrypted && !_dbEncryption.VerifyPassword(AppBootstrapper.DbPath, CurrentDbPassword))
|
||||
{
|
||||
DbPasswordError = "Aktuelles Passwort ist falsch.";
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!wantsPassword)
|
||||
{
|
||||
DbPasswordError = "Bitte ein neues Passwort eingeben.";
|
||||
valid = false;
|
||||
}
|
||||
else if (NewDbPassword != NewDbPasswordConfirm)
|
||||
{
|
||||
DbPasswordError = "Neue Passwörter stimmen nicht überein.";
|
||||
valid = false;
|
||||
}
|
||||
else if (NewDbPassword.Length < 4)
|
||||
{
|
||||
DbPasswordError = "Mindestens 4 Zeichen.";
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
_dbContext.Dispose();
|
||||
_dbEncryption.SetPassword(AppBootstrapper.DbPath, IsDbEncrypted ? CurrentDbPassword : null, NewDbPassword.Trim());
|
||||
AppBootstrapper.RestartApplication();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveDbPassword()
|
||||
{
|
||||
DbPasswordError = "";
|
||||
if (!_dbEncryption.VerifyPassword(AppBootstrapper.DbPath, CurrentDbPassword))
|
||||
{
|
||||
DbPasswordError = "Aktuelles Passwort ist falsch.";
|
||||
return;
|
||||
}
|
||||
|
||||
_dbContext.Dispose();
|
||||
_dbEncryption.SetPassword(AppBootstrapper.DbPath, CurrentDbPassword, null);
|
||||
AppBootstrapper.RestartApplication();
|
||||
}
|
||||
|
||||
// ── App-Sperre: Speichern ─────────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveAppLockSettings()
|
||||
{
|
||||
AppLockPasswordError = "";
|
||||
var valid = true;
|
||||
var wantsNewPassword = !string.IsNullOrWhiteSpace(AppLockNewPassword) || !string.IsNullOrWhiteSpace(AppLockNewPasswordConfirm);
|
||||
|
||||
if (AppLockEnabled && !_appLock.HasPassword && !wantsNewPassword)
|
||||
{
|
||||
AppLockPasswordError = "Bitte ein Passwort für die App-Sperre festlegen.";
|
||||
valid = false;
|
||||
}
|
||||
else if (wantsNewPassword)
|
||||
{
|
||||
if (AppLockNewPassword != AppLockNewPasswordConfirm)
|
||||
{
|
||||
AppLockPasswordError = "Passwörter stimmen nicht überein.";
|
||||
valid = false;
|
||||
}
|
||||
else if (AppLockNewPassword.Length < 4)
|
||||
{
|
||||
AppLockPasswordError = "Mindestens 4 Zeichen.";
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
if (wantsNewPassword) _appLock.SetPassword(AppLockNewPassword.Trim());
|
||||
_appLock.Configure(AppLockEnabled, AppLockTimeoutMinutes);
|
||||
|
||||
AppLockNewPassword = ""; AppLockNewPasswordConfirm = "";
|
||||
OnPropertyChanged(nameof(AppLockHasPassword));
|
||||
AppLockStatus = "Gespeichert.";
|
||||
OnAppLockChanged?.Invoke();
|
||||
}
|
||||
|
||||
// ── Gewichtungsschema-Voreinstellungen: Laden ────────────────────────────
|
||||
@@ -486,6 +654,12 @@ public class SubjectListItem(Subject s)
|
||||
public string ShortName { get; } = s.ShortName;
|
||||
}
|
||||
|
||||
public class BackupListItem(BackupInfo info)
|
||||
{
|
||||
public string Path { get; } = info.Path;
|
||||
public string Display { get; } = $"{info.CreatedAt:dd.MM.yyyy HH:mm} · {info.SizeBytes / 1024.0:0} KB";
|
||||
}
|
||||
|
||||
// ── JSON DTOs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
internal class CatalogDto
|
||||
|
||||
Reference in New Issue
Block a user