Fixed buttons and reset logic, databank is completly backed up and wiped now

This commit is contained in:
BeatriceAl
2026-05-11 23:20:45 +02:00
parent 9d22aa974c
commit 4638714494
8 changed files with 140 additions and 39 deletions
+1 -1
View File
@@ -121,7 +121,7 @@
<section class="card shadow-sm border-danger-subtle">
<div class="card-body">
<h2 class="h5 card-title">Reset setup</h2>
<p class="text-secondary">Move existing users to backup and mark setup as incomplete.</p>
<p class="text-secondary">Copy the current database to a timestamped backup database and start setup with a fresh database.</p>
<form method="post" action="/admin/reset-setup">
<button type="submit" class="btn btn-outline-danger">
+11 -3
View File
@@ -13,6 +13,12 @@ public partial class Admin
[Inject]
protected AccessCodeService AccessCodeService { get; set; } = default!;
[Inject]
protected DatabaseBackupService DatabaseBackupService { get; set; } = default!;
[Inject]
protected GuestEntryService GuestEntryService { get; set; } = default!;
protected string Message { get; set; } = "";
protected string NewGuestAccessCode { get; set; } = "";
@@ -61,16 +67,18 @@ public partial class Admin
try
{
await UserService.BackupAndClearUsersAsync("Setup reset from admin panel");
var backupResult = await DatabaseBackupService.BackupAndDropActiveDatabaseAsync("Setup reset from admin panel");
await SetupStatusService.ResetSetupAsync();
await UserService.EnsureIndexesAsync();
await GuestEntryService.EnsureIndexesAsync();
await AccessCodeService.EnsureIndexesAsync();
Message = $"Setup has been reset. The old database was copied to {backupResult.BackupDatabaseName}.";
}
catch
{
Message = "Setup could not be reset.";
return;
}
Message = "Setup has been reset. Existing users were moved to backup.";
}
protected async Task UpdateGuestAccessCode()
+11 -13
View File
@@ -6,7 +6,7 @@
<p class="text-secondary mb-0">Create the first admin and initial guest access code.</p>
</div>
@if (IsSetupComplete is null || HasAnyUsers is null || HasAnyUserBackups is null)
@if (IsSetupComplete is null || HasAnyUsers is null || HasAnyDatabaseBackups is null)
{
<section class="card shadow-sm">
<div class="card-body">
@@ -40,9 +40,9 @@ else if (HasAnyUsers == true)
}
else
{
<form class="admin-panel-grid" method="post" action="/setup/complete">
<div class="admin-panel-grid">
<section class="card shadow-sm">
<div class="card-body">
<form class="card-body" method="post" action="/setup/complete">
<h2 class="h5 card-title">First admin</h2>
<div class="mb-3">
@@ -59,11 +59,7 @@ else
<label class="form-label" for="setup-password">Password</label>
<input id="setup-password" class="form-control" type="password" name="password" />
</div>
</div>
</section>
<section class="card shadow-sm">
<div class="card-body">
<h2 class="h5 card-title">Guest access</h2>
<p class="text-secondary">Set the first code guests use to view the list.</p>
@@ -80,20 +76,22 @@ else
{
<div class="alert alert-warning mt-3 mb-0" role="alert">@ErrorMessage</div>
}
</div>
</form>
</section>
@if (HasAnyUserBackups == true)
@if (HasAnyDatabaseBackups == true)
{
<section class="card shadow-sm">
<div class="card-body">
<h2 class="h5 card-title">Restore backup</h2>
<p class="text-secondary">Restore the latest backed-up users.</p>
<button type="button" class="btn btn-outline-primary" @onclick="RestoreLatestBackup">
Restore latest user backup
<p class="text-secondary">Restore the latest database backup created by setup reset.</p>
<form method="post" action="/setup/restore">
<button type="submit" class="btn btn-outline-primary">
Restore latest database backup
</button>
</form>
</div>
</section>
}
</form>
</div>
}
+33 -12
View File
@@ -13,13 +13,19 @@ public partial class Setup : ComponentBase
[Inject]
protected AccessCodeService AccessCodeService { get; set; } = default!;
[Inject]
protected GuestEntryService GuestEntryService { get; set; } = default!;
[Inject]
protected DatabaseBackupService DatabaseBackupService { get; set; } = default!;
protected string Username { get; set; } = "";
protected string DisplayName { get; set; } = "";
protected string Password { get; set; } = "";
protected string ErrorMessage { get; set; } = "";
protected bool? HasAnyUsers { get; set; }
protected bool? HasAnyUserBackups { get; set; }
protected bool? HasAnyDatabaseBackups { get; set; }
protected string GuestAccessCode { get; set; } = "";
@@ -28,6 +34,15 @@ public partial class Setup : ComponentBase
[SupplyParameterFromQuery(Name = "error")]
public string? SetupError { get; set; }
[SupplyParameterFromQuery(Name = "backupDatabase")]
public string? BackupDatabase { get; set; }
[SupplyParameterFromQuery(Name = "backedUpCollections")]
public int? BackedUpCollections { get; set; }
[SupplyParameterFromQuery(Name = "backedUpDocuments")]
public long? BackedUpDocuments { get; set; }
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrWhiteSpace(SetupError))
@@ -36,6 +51,9 @@ public partial class Setup : ComponentBase
{
"existing-user" => "Setup cannot create another initial admin user.",
"setup-failed" => "Setup could not be completed. No partial admin user was kept.",
"setup-reset" => $"Setup has been reset. The old database was copied to {BackupDatabase ?? "a backup database"}. {BackedUpCollections ?? 0} collections and {BackedUpDocuments ?? 0} documents were backed up.",
"restore-users-exist" => "Cannot restore backup while users already exist.",
"restore-failed" => "Could not restore latest database backup.",
_ => SetupError
};
}
@@ -44,14 +62,14 @@ public partial class Setup : ComponentBase
{
IsSetupComplete = await SetupStatusService.IsSetupCompleteAsync();
HasAnyUsers = await UserService.HasAnyUsersAsync();
HasAnyUserBackups = await UserService.HasAnyUserBackupsAsync();
HasAnyDatabaseBackups = await DatabaseBackupService.HasAnyDatabaseBackupsAsync();
}
catch
{
ErrorMessage = "Setup status could not be loaded. Please try again.";
IsSetupComplete = false;
HasAnyUsers = false;
HasAnyUserBackups = false;
HasAnyDatabaseBackups = false;
}
}
@@ -137,33 +155,36 @@ public partial class Setup : ComponentBase
return;
}
if (HasAnyUserBackups != true)
if (HasAnyDatabaseBackups != true)
{
ErrorMessage = "No user backups found.";
ErrorMessage = "No database backups found.";
return;
}
bool restored;
DatabaseBackupResult? restored;
try
{
restored = await UserService.RestoreLatestUserBackupAsync();
restored = await DatabaseBackupService.RestoreLatestDatabaseBackupAsync();
}
catch
{
ErrorMessage = "Could not restore latest user backup.";
ErrorMessage = "Could not restore latest database backup.";
return;
}
if (!restored)
if (restored is null)
{
ErrorMessage = "Could not restore latest user backup.";
ErrorMessage = "Could not restore latest database backup.";
return;
}
try
{
await SetupStatusService.MarkSetupCompleteAsync();
await UserService.EnsureIndexesAsync();
await GuestEntryService.EnsureIndexesAsync();
await AccessCodeService.EnsureIndexesAsync();
}
catch
{
@@ -173,8 +194,8 @@ public partial class Setup : ComponentBase
HasAnyUsers = true;
IsSetupComplete = true;
HasAnyUserBackups = await UserService.HasAnyUserBackupsAsync();
HasAnyDatabaseBackups = await DatabaseBackupService.HasAnyDatabaseBackupsAsync();
ErrorMessage = "Latest user backup has been restored.";
ErrorMessage = "Latest database backup has been restored.";
}
}
+5 -3
View File
@@ -11,12 +11,14 @@ public sealed class MongoOptions
public sealed class MongoDbContext
{
public IMongoClient Client { get; }
public IMongoDatabase Database { get; }
public string DatabaseName { get; }
public MongoDbContext(IOptions<MongoOptions> options)
{
var client = new MongoClient(options.Value.ConnectionString);
Database = client.GetDatabase(options.Value.DatabaseName);
Client = new MongoClient(options.Value.ConnectionString);
DatabaseName = options.Value.DatabaseName;
Database = Client.GetDatabase(DatabaseName);
}
}
+60 -4
View File
@@ -21,6 +21,7 @@ builder.Services.AddScoped<SetupStatusService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<GuestEntryService>();
builder.Services.AddScoped<AccessCodeService>();
builder.Services.AddScoped<DatabaseBackupService>();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
@@ -233,6 +234,40 @@ app.MapPost("/setup/complete", async (
return Results.Redirect("/login");
});
app.MapPost("/setup/restore", async (
DatabaseBackupService databaseBackupService,
UserService userService,
GuestEntryService guestEntryService,
AccessCodeService accessCodeService,
SetupStatusService setupStatusService) =>
{
try
{
if (await userService.HasAnyUsersAsync())
{
return Results.Redirect("/setup?error=restore-users-exist");
}
var restored = await databaseBackupService.RestoreLatestDatabaseBackupAsync();
if (restored is null)
{
return Results.Redirect("/setup?error=restore-failed");
}
await setupStatusService.MarkSetupCompleteAsync();
await userService.EnsureIndexesAsync();
await guestEntryService.EnsureIndexesAsync();
await accessCodeService.EnsureIndexesAsync();
}
catch
{
return Results.Redirect("/setup?error=restore-failed");
}
return Results.Redirect("/login");
});
app.MapPost("/guests/add", async (
HttpContext httpContext,
GuestEntryService guestEntryService) =>
@@ -406,20 +441,40 @@ app.MapPost("/admin/maintainers/delete", async (
}).RequireAuthorization("AdminOnly");
app.MapPost("/admin/reset-setup", async (
HttpContext httpContext,
DatabaseBackupService databaseBackupService,
UserService userService,
GuestEntryService guestEntryService,
AccessCodeService accessCodeService,
SetupStatusService setupStatusService) =>
{
DatabaseBackupResult backupResult;
try
{
await userService.BackupAndClearUsersAsync("Setup reset from admin panel");
backupResult = await databaseBackupService.BackupAndDropActiveDatabaseAsync("Setup reset from admin panel");
await setupStatusService.ResetSetupAsync();
await userService.EnsureIndexesAsync();
await guestEntryService.EnsureIndexesAsync();
await accessCodeService.EnsureIndexesAsync();
await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
httpContext.Session.Clear();
}
catch
catch (InvalidOperationException exception)
{
return Results.Redirect("/admin?message=Setup could not be reset.");
var error = Uri.EscapeDataString(exception.Message);
return Results.Redirect($"/admin?message={error}");
}
catch (Exception exception)
{
var error = Uri.EscapeDataString($"Setup could not be reset: {exception.Message}");
return Results.Redirect($"/admin?message={error}");
}
return Results.Redirect("/admin?message=Setup has been reset. Existing users were moved to backup.");
var backupDatabase = Uri.EscapeDataString(backupResult.BackupDatabaseName);
return Results.Redirect(
$"/setup?error=setup-reset&backupDatabase={backupDatabase}&backedUpCollections={backupResult.CollectionCount}&backedUpDocuments={backupResult.DocumentCount}");
}).RequireAuthorization("AdminOnly");
app.MapPost("/admin/setup-recovery", async (
@@ -480,6 +535,7 @@ app.Use(async (context, next) =>
isStaticFile ||
path.StartsWithSegments("/setup") ||
path.StartsWithSegments("/setup/complete") ||
path.StartsWithSegments("/setup/restore") ||
path.StartsWithSegments("/auth/logout") ||
path.StartsWithSegments("/guest/logout") ||
path.StartsWithSegments("/_framework") ||
+16 -2
View File
@@ -70,7 +70,7 @@ public sealed class UserService(MongoDbContext db)
public async Task<bool> HasAnyUsersAsync()
=> await _users.CountDocumentsAsync(FilterDefinition<UserDocument>.Empty) > 0;
public async Task BackupAndClearUsersAsync(string reason)
public async Task<int> BackupAndClearUsersAsync(string reason)
{
var users = await _users
.Find(FilterDefinition<UserDocument>.Empty)
@@ -92,7 +92,21 @@ public sealed class UserService(MongoDbContext db)
await _userBackups.InsertManyAsync(backups);
}
await _users.DeleteManyAsync(FilterDefinition<UserDocument>.Empty);
var deleteResult = await _users.DeleteManyAsync(FilterDefinition<UserDocument>.Empty);
if (!deleteResult.IsAcknowledged)
{
throw new InvalidOperationException("MongoDB did not acknowledge user deletion.");
}
var remainingUsers = await _users.CountDocumentsAsync(FilterDefinition<UserDocument>.Empty);
if (remainingUsers > 0)
{
throw new InvalidOperationException("Users could not be cleared.");
}
return users.Count;
}
public async Task<bool> HasAnyUserBackupsAsync()
+2
View File
@@ -53,3 +53,5 @@ ASPNETCORE_URLS: http://+:8080
```
MongoDB-Daten liegen im Compose-Volume `mongo_data`.
Beim Reset im Admin-Bereich kopiert die App die aktuelle Datenbank zuerst in eine neue Backup-Datenbank mit dem Muster `guestlist_backup_<timestamp>`. Danach wird die aktive Datenbank frisch angelegt und das Setup ist wieder offen. Auf der Setup-Seite kann das letzte Datenbank-Backup wiederhergestellt werden.