Fixed buttons and reset logic, databank is completly backed up and wiped now
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -13,6 +13,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; } = "";
|
||||||
|
|
||||||
protected string NewGuestAccessCode { get; set; } = "";
|
protected string NewGuestAccessCode { 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()
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,19 @@ 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; } = "";
|
||||||
protected string Password { get; set; } = "";
|
protected string Password { get; set; } = "";
|
||||||
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.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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") ||
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user