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"> <section class="card shadow-sm border-danger-subtle">
<div class="card-body"> <div class="card-body">
<h2 class="h5 card-title">Reset setup</h2> <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"> <form method="post" action="/admin/reset-setup">
<button type="submit" class="btn btn-outline-danger"> <button type="submit" class="btn btn-outline-danger">
+11 -3
View File
@@ -12,6 +12,12 @@ public partial class Admin
[Inject] [Inject]
protected AccessCodeService AccessCodeService { get; set; } = default!; 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 Message { get; set; } = "";
@@ -61,16 +67,18 @@ public partial class Admin
try try
{ {
await UserService.BackupAndClearUsersAsync("Setup reset from admin panel"); var backupResult = await DatabaseBackupService.BackupAndDropActiveDatabaseAsync("Setup reset from admin panel");
await SetupStatusService.ResetSetupAsync(); 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 catch
{ {
Message = "Setup could not be reset."; Message = "Setup could not be reset.";
return; return;
} }
Message = "Setup has been reset. Existing users were moved to backup.";
} }
protected async Task UpdateGuestAccessCode() protected async Task UpdateGuestAccessCode()
+12 -14
View File
@@ -6,7 +6,7 @@
<p class="text-secondary mb-0">Create the first admin and initial guest access code.</p> <p class="text-secondary mb-0">Create the first admin and initial guest access code.</p>
</div> </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"> <section class="card shadow-sm">
<div class="card-body"> <div class="card-body">
@@ -40,9 +40,9 @@ else if (HasAnyUsers == true)
} }
else else
{ {
<form class="admin-panel-grid" method="post" action="/setup/complete"> <div class="admin-panel-grid">
<section class="card shadow-sm"> <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> <h2 class="h5 card-title">First admin</h2>
<div class="mb-3"> <div class="mb-3">
@@ -59,11 +59,7 @@ else
<label class="form-label" for="setup-password">Password</label> <label class="form-label" for="setup-password">Password</label>
<input id="setup-password" class="form-control" type="password" name="password" /> <input id="setup-password" class="form-control" type="password" name="password" />
</div> </div>
</div>
</section>
<section class="card shadow-sm">
<div class="card-body">
<h2 class="h5 card-title">Guest access</h2> <h2 class="h5 card-title">Guest access</h2>
<p class="text-secondary">Set the first code guests use to view the list.</p> <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 class="alert alert-warning mt-3 mb-0" role="alert">@ErrorMessage</div>
} }
</div> </form>
</section> </section>
@if (HasAnyUserBackups == true) @if (HasAnyDatabaseBackups == true)
{ {
<section class="card shadow-sm"> <section class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<h2 class="h5 card-title">Restore backup</h2> <h2 class="h5 card-title">Restore backup</h2>
<p class="text-secondary">Restore the latest backed-up users.</p> <p class="text-secondary">Restore the latest database backup created by setup reset.</p>
<button type="button" class="btn btn-outline-primary" @onclick="RestoreLatestBackup"> <form method="post" action="/setup/restore">
Restore latest user backup <button type="submit" class="btn btn-outline-primary">
</button> Restore latest database backup
</button>
</form>
</div> </div>
</section> </section>
} }
</form> </div>
} }
+33 -12
View File
@@ -12,6 +12,12 @@ public partial class Setup : ComponentBase
[Inject] [Inject]
protected AccessCodeService AccessCodeService { get; set; } = default!; 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 Username { get; set; } = "";
protected string DisplayName { get; set; } = ""; protected string DisplayName { get; set; } = "";
@@ -19,7 +25,7 @@ public partial class Setup : ComponentBase
protected string ErrorMessage { get; set; } = ""; protected string ErrorMessage { get; set; } = "";
protected bool? HasAnyUsers { get; set; } protected bool? HasAnyUsers { get; set; }
protected bool? HasAnyUserBackups { get; set; } protected bool? HasAnyDatabaseBackups { get; set; }
protected string GuestAccessCode { get; set; } = ""; protected string GuestAccessCode { get; set; } = "";
@@ -28,6 +34,15 @@ public partial class Setup : ComponentBase
[SupplyParameterFromQuery(Name = "error")] [SupplyParameterFromQuery(Name = "error")]
public string? SetupError { get; set; } 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() protected override async Task OnInitializedAsync()
{ {
if (!string.IsNullOrWhiteSpace(SetupError)) if (!string.IsNullOrWhiteSpace(SetupError))
@@ -36,6 +51,9 @@ public partial class Setup : ComponentBase
{ {
"existing-user" => "Setup cannot create another initial admin user.", "existing-user" => "Setup cannot create another initial admin user.",
"setup-failed" => "Setup could not be completed. No partial admin user was kept.", "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 _ => SetupError
}; };
} }
@@ -44,14 +62,14 @@ public partial class Setup : ComponentBase
{ {
IsSetupComplete = await SetupStatusService.IsSetupCompleteAsync(); IsSetupComplete = await SetupStatusService.IsSetupCompleteAsync();
HasAnyUsers = await UserService.HasAnyUsersAsync(); HasAnyUsers = await UserService.HasAnyUsersAsync();
HasAnyUserBackups = await UserService.HasAnyUserBackupsAsync(); HasAnyDatabaseBackups = await DatabaseBackupService.HasAnyDatabaseBackupsAsync();
} }
catch catch
{ {
ErrorMessage = "Setup status could not be loaded. Please try again."; ErrorMessage = "Setup status could not be loaded. Please try again.";
IsSetupComplete = false; IsSetupComplete = false;
HasAnyUsers = false; HasAnyUsers = false;
HasAnyUserBackups = false; HasAnyDatabaseBackups = false;
} }
} }
@@ -137,33 +155,36 @@ public partial class Setup : ComponentBase
return; return;
} }
if (HasAnyUserBackups != true) if (HasAnyDatabaseBackups != true)
{ {
ErrorMessage = "No user backups found."; ErrorMessage = "No database backups found.";
return; return;
} }
bool restored; DatabaseBackupResult? restored;
try try
{ {
restored = await UserService.RestoreLatestUserBackupAsync(); restored = await DatabaseBackupService.RestoreLatestDatabaseBackupAsync();
} }
catch catch
{ {
ErrorMessage = "Could not restore latest user backup."; ErrorMessage = "Could not restore latest database backup.";
return; return;
} }
if (!restored) if (restored is null)
{ {
ErrorMessage = "Could not restore latest user backup."; ErrorMessage = "Could not restore latest database backup.";
return; return;
} }
try try
{ {
await SetupStatusService.MarkSetupCompleteAsync(); await SetupStatusService.MarkSetupCompleteAsync();
await UserService.EnsureIndexesAsync();
await GuestEntryService.EnsureIndexesAsync();
await AccessCodeService.EnsureIndexesAsync();
} }
catch catch
{ {
@@ -173,8 +194,8 @@ public partial class Setup : ComponentBase
HasAnyUsers = true; HasAnyUsers = true;
IsSetupComplete = 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 sealed class MongoDbContext
{ {
public IMongoClient Client { get; }
public IMongoDatabase Database { get; } public IMongoDatabase Database { get; }
public string DatabaseName { get; }
public MongoDbContext(IOptions<MongoOptions> options) public MongoDbContext(IOptions<MongoOptions> options)
{ {
var client = new MongoClient(options.Value.ConnectionString); Client = new MongoClient(options.Value.ConnectionString);
Database = client.GetDatabase(options.Value.DatabaseName); 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<UserService>();
builder.Services.AddScoped<GuestEntryService>(); builder.Services.AddScoped<GuestEntryService>();
builder.Services.AddScoped<AccessCodeService>(); builder.Services.AddScoped<AccessCodeService>();
builder.Services.AddScoped<DatabaseBackupService>();
builder.Services builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
@@ -233,6 +234,40 @@ app.MapPost("/setup/complete", async (
return Results.Redirect("/login"); 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 ( app.MapPost("/guests/add", async (
HttpContext httpContext, HttpContext httpContext,
GuestEntryService guestEntryService) => GuestEntryService guestEntryService) =>
@@ -406,20 +441,40 @@ app.MapPost("/admin/maintainers/delete", async (
}).RequireAuthorization("AdminOnly"); }).RequireAuthorization("AdminOnly");
app.MapPost("/admin/reset-setup", async ( app.MapPost("/admin/reset-setup", async (
HttpContext httpContext,
DatabaseBackupService databaseBackupService,
UserService userService, UserService userService,
GuestEntryService guestEntryService,
AccessCodeService accessCodeService,
SetupStatusService setupStatusService) => SetupStatusService setupStatusService) =>
{ {
DatabaseBackupResult backupResult;
try try
{ {
await userService.BackupAndClearUsersAsync("Setup reset from admin panel"); backupResult = await databaseBackupService.BackupAndDropActiveDatabaseAsync("Setup reset from admin panel");
await setupStatusService.ResetSetupAsync(); 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"); }).RequireAuthorization("AdminOnly");
app.MapPost("/admin/setup-recovery", async ( app.MapPost("/admin/setup-recovery", async (
@@ -480,6 +535,7 @@ app.Use(async (context, next) =>
isStaticFile || isStaticFile ||
path.StartsWithSegments("/setup") || path.StartsWithSegments("/setup") ||
path.StartsWithSegments("/setup/complete") || path.StartsWithSegments("/setup/complete") ||
path.StartsWithSegments("/setup/restore") ||
path.StartsWithSegments("/auth/logout") || path.StartsWithSegments("/auth/logout") ||
path.StartsWithSegments("/guest/logout") || path.StartsWithSegments("/guest/logout") ||
path.StartsWithSegments("/_framework") || path.StartsWithSegments("/_framework") ||
+16 -2
View File
@@ -70,7 +70,7 @@ public sealed class UserService(MongoDbContext db)
public async Task<bool> HasAnyUsersAsync() public async Task<bool> HasAnyUsersAsync()
=> await _users.CountDocumentsAsync(FilterDefinition<UserDocument>.Empty) > 0; => await _users.CountDocumentsAsync(FilterDefinition<UserDocument>.Empty) > 0;
public async Task BackupAndClearUsersAsync(string reason) public async Task<int> BackupAndClearUsersAsync(string reason)
{ {
var users = await _users var users = await _users
.Find(FilterDefinition<UserDocument>.Empty) .Find(FilterDefinition<UserDocument>.Empty)
@@ -92,7 +92,21 @@ public sealed class UserService(MongoDbContext db)
await _userBackups.InsertManyAsync(backups); 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() public async Task<bool> HasAnyUserBackupsAsync()
+2
View File
@@ -53,3 +53,5 @@ ASPNETCORE_URLS: http://+:8080
``` ```
MongoDB-Daten liegen im Compose-Volume `mongo_data`. 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.