Fixed most buttons @onclick -> form

This commit is contained in:
BeatriceAl
2026-05-11 22:41:55 +02:00
parent 7698ccd6ae
commit 9d22aa974c
7 changed files with 379 additions and 37 deletions
+22 -15
View File
@@ -14,10 +14,10 @@
<h2 class="h5 card-title">Guest access</h2>
<p class="text-secondary">Change the code guests use to view the list.</p>
<form @onsubmit="UpdateGuestAccessCode" @onsubmit:preventDefault="true">
<form method="post" action="/admin/guest-access-code">
<div class="mb-3">
<label class="form-label" for="guest-access-code">New guest access code</label>
<input id="guest-access-code" class="form-control" @bind="NewGuestAccessCode" />
<input id="guest-access-code" class="form-control" name="guestAccessCode" />
</div>
<button type="submit" class="btn btn-primary">
@@ -37,20 +37,20 @@
<h2 class="h5 card-title">Create maintainer</h2>
<p class="text-secondary">Maintainers can add, edit and delete guests.</p>
<form @onsubmit="CreateMaintainer" @onsubmit:preventDefault="true">
<form method="post" action="/admin/maintainers/create">
<div class="mb-3">
<label class="form-label" for="maintainer-username">Username</label>
<input id="maintainer-username" class="form-control" @bind="MaintainerUsername" />
<input id="maintainer-username" class="form-control" name="username" />
</div>
<div class="mb-3">
<label class="form-label" for="maintainer-name">Name</label>
<input id="maintainer-name" class="form-control" @bind="MaintainerDisplayName" />
<input id="maintainer-name" class="form-control" name="displayName" />
</div>
<div class="mb-3">
<label class="form-label" for="maintainer-password">Password</label>
<input id="maintainer-password" class="form-control" type="password" @bind="MaintainerPassword" />
<input id="maintainer-password" class="form-control" type="password" name="password" />
</div>
<button type="submit" class="btn btn-primary">
@@ -87,9 +87,12 @@
<div class="fw-semibold">@maintainer.DisplayName</div>
<div class="small text-secondary">@maintainer.Username</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="() => DeleteMaintainer(maintainer.Id)">
Delete
</button>
<form method="post" action="/admin/maintainers/delete">
<input type="hidden" name="id" value="@maintainer.Id" />
<button type="submit" class="btn btn-outline-danger btn-sm">
Delete
</button>
</form>
</div>
}
</div>
@@ -102,9 +105,11 @@
<h2 class="h5 card-title">Setup recovery</h2>
<p class="text-secondary">Use this after a partial setup if admin access exists but setup is incomplete.</p>
<button type="button" class="btn btn-outline-primary" @onclick="CompleteSetupRecovery">
Complete setup recovery
</button>
<form method="post" action="/admin/setup-recovery">
<button type="submit" class="btn btn-outline-primary">
Complete setup recovery
</button>
</form>
@if (!string.IsNullOrWhiteSpace(RecoveryMessage))
{
@@ -118,9 +123,11 @@
<h2 class="h5 card-title">Reset setup</h2>
<p class="text-secondary">Move existing users to backup and mark setup as incomplete.</p>
<button type="button" class="btn btn-outline-danger" @onclick="ResetSetup">
Reset setup
</button>
<form method="post" action="/admin/reset-setup">
<button type="submit" class="btn btn-outline-danger">
Reset setup
</button>
</form>
@if (!string.IsNullOrWhiteSpace(Message))
{
+17
View File
@@ -25,8 +25,25 @@ public partial class Admin
protected string MaintainerMessage { get; set; } = "";
protected List<UserDocument>? Maintainers { get; set; }
[SupplyParameterFromQuery(Name = "message")]
public string? IncomingMessage { get; set; }
[SupplyParameterFromQuery(Name = "accessCodeMessage")]
public string? IncomingAccessCodeMessage { get; set; }
[SupplyParameterFromQuery(Name = "maintainerMessage")]
public string? IncomingMaintainerMessage { get; set; }
[SupplyParameterFromQuery(Name = "recoveryMessage")]
public string? IncomingRecoveryMessage { get; set; }
protected override async Task OnInitializedAsync()
{
Message = IncomingMessage ?? "";
AccessCodeMessage = IncomingAccessCodeMessage ?? "";
MaintainerMessage = IncomingMaintainerMessage ?? "";
RecoveryMessage = IncomingRecoveryMessage ?? "";
try
{
Maintainers = await UserService.GetMaintainersAsync();
+30 -16
View File
@@ -9,10 +9,19 @@
<p class="text-secondary mb-0">Add, edit, delete and filter guest entries.</p>
</div>
<div class="w-100 w-md-auto">
<form method="get" action="/guests" class="w-100 w-md-auto">
<label class="form-label" for="guest-search">Search</label>
<input id="guest-search" class="form-control" @bind="SearchTerm" @bind:event="oninput" placeholder="Name, note or user" />
</div>
<div class="d-flex gap-2">
<input id="guest-search" class="form-control" name="search" value="@SearchTerm" placeholder="Name, note or user" />
<button type="submit" class="btn btn-outline-primary">
Search
</button>
@if (!string.IsNullOrWhiteSpace(SearchTerm))
{
<a class="btn btn-outline-secondary" href="/guests">Clear</a>
}
</div>
</form>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
@@ -24,15 +33,15 @@
<article class="card shadow-sm">
<div class="card-body">
<h2 class="h5 card-title">Add guest</h2>
<form @onsubmit="AddGuest" @onsubmit:preventDefault="true">
<form method="post" action="/guests/add">
<div class="mb-3">
<label class="form-label" for="new-guest-name">Name</label>
<input id="new-guest-name" class="form-control" @bind="NewGuestName" />
<input id="new-guest-name" class="form-control" name="name" />
</div>
<div class="mb-3">
<label class="form-label" for="new-guest-notes">Notes</label>
<textarea id="new-guest-notes" class="form-control" rows="3" @bind="NewGuestNotes"></textarea>
<textarea id="new-guest-notes" class="form-control" rows="3" name="notes"></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">
@@ -76,15 +85,17 @@
{
<h2 class="h5 card-title">Edit guest</h2>
<form class="d-flex flex-column flex-grow-1" @onsubmit="SaveEdit" @onsubmit:preventDefault="true">
<form class="d-flex flex-column flex-grow-1" method="post" action="/guests/update">
<input type="hidden" name="id" value="@guest.Id" />
<div class="mb-3">
<label class="form-label">Name</label>
<input class="form-control" @bind="EditGuestName" />
<input class="form-control" name="name" value="@guest.Name" />
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea class="form-control" rows="3" @bind="EditGuestNotes"></textarea>
<textarea class="form-control" rows="3" name="notes">@guest.Notes</textarea>
</div>
<div class="d-flex gap-2 mt-auto">
@@ -92,9 +103,9 @@
Save
</button>
<button type="button" class="btn btn-outline-secondary" @onclick="CancelEdit">
<a class="btn btn-outline-secondary" href="/guests">
Cancel
</button>
</a>
</div>
</form>
}
@@ -117,13 +128,16 @@
</p>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="() => StartEdit(guest)">
<a class="btn btn-outline-primary btn-sm" href="/guests?edit=@guest.Id">
Edit
</button>
</a>
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="() => DeleteGuest(guest.Id)">
Delete
</button>
<form method="post" action="/guests/delete">
<input type="hidden" name="id" value="@guest.Id" />
<button type="submit" class="btn btn-outline-danger btn-sm">
Delete
</button>
</form>
</div>
}
</div>
@@ -23,6 +23,15 @@ public partial class Guests : ComponentBase
protected List<GuestEntryDocument>? GuestEntries { get; set; }
[SupplyParameterFromQuery(Name = "error")]
public string? IncomingError { get; set; }
[SupplyParameterFromQuery(Name = "search")]
public string? IncomingSearchTerm { get; set; }
[SupplyParameterFromQuery(Name = "edit")]
public string? IncomingEditingGuestId { get; set; }
protected IEnumerable<GuestEntryDocument> FilteredGuestEntries
{
get
@@ -48,6 +57,17 @@ public partial class Guests : ComponentBase
protected override async Task OnInitializedAsync()
{
SearchTerm = IncomingSearchTerm ?? "";
EditingGuestId = IncomingEditingGuestId;
ErrorMessage = IncomingError switch
{
"duplicate" => "A guest with this name and note already exists. Use a distinct note if this is a different person.",
"add-failed" => "Guest could not be added.",
null or "" => "",
_ => IncomingError
};
try
{
GuestEntries = await GuestEntryService.GetAllAsync();
+5 -5
View File
@@ -40,24 +40,24 @@ else if (HasAnyUsers == true)
}
else
{
<form class="admin-panel-grid" @onsubmit="CompleteSetup" @onsubmit:preventDefault="true">
<form class="admin-panel-grid" method="post" action="/setup/complete">
<section class="card shadow-sm">
<div class="card-body">
<h2 class="h5 card-title">First admin</h2>
<div class="mb-3">
<label class="form-label" for="setup-username">Username</label>
<input id="setup-username" class="form-control" @bind="Username" />
<input id="setup-username" class="form-control" name="username" />
</div>
<div class="mb-3">
<label class="form-label" for="setup-name">Name</label>
<input id="setup-name" class="form-control" @bind="DisplayName" />
<input id="setup-name" class="form-control" name="displayName" />
</div>
<div class="mb-3">
<label class="form-label" for="setup-password">Password</label>
<input id="setup-password" class="form-control" type="password" @bind="Password" />
<input id="setup-password" class="form-control" type="password" name="password" />
</div>
</div>
</section>
@@ -69,7 +69,7 @@ else
<div class="mb-3">
<label class="form-label" for="setup-access-code">Guest access code</label>
<input id="setup-access-code" class="form-control" @bind="GuestAccessCode" />
<input id="setup-access-code" class="form-control" name="guestAccessCode" />
</div>
<button type="submit" class="btn btn-primary">
+13
View File
@@ -25,8 +25,21 @@ public partial class Setup : ComponentBase
protected bool? IsSetupComplete { get; set; }
[SupplyParameterFromQuery(Name = "error")]
public string? SetupError { get; set; }
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrWhiteSpace(SetupError))
{
ErrorMessage = SetupError switch
{
"existing-user" => "Setup cannot create another initial admin user.",
"setup-failed" => "Setup could not be completed. No partial admin user was kept.",
_ => SetupError
};
}
try
{
IsSetupComplete = await SetupStatusService.IsSetupCompleteAsync();
+272 -1
View File
@@ -35,7 +35,11 @@ builder.Services
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
builder.Services.AddAuthorization();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("StaffOnly", policy => policy.RequireRole(UserRoles.Admin, UserRoles.Maintainer));
options.AddPolicy("AdminOnly", policy => policy.RequireRole(UserRoles.Admin));
});
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();
@@ -173,6 +177,272 @@ app.MapPost("/guest/logout", (HttpContext httpContext) =>
return Results.Redirect("/guest");
});
app.MapPost("/setup/complete", async (
HttpContext httpContext,
UserService userService,
AccessCodeService accessCodeService,
SetupStatusService setupStatusService) =>
{
var form = await httpContext.Request.ReadFormAsync();
var username = form["username"].ToString();
var displayName = form["displayName"].ToString();
var password = form["password"].ToString();
var guestAccessCode = form["guestAccessCode"].ToString();
if (await userService.HasAnyUsersAsync())
{
return Results.Redirect("/setup?error=existing-user");
}
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = username;
}
var adminCreated = false;
try
{
await userService.CreateAdminAsync(displayName, username, password);
adminCreated = true;
await accessCodeService.SetGuestAccessCodeAsync(guestAccessCode);
await setupStatusService.MarkSetupCompleteAsync();
}
catch (InvalidOperationException exception)
{
if (adminCreated)
{
await userService.DeleteUserByUsernameAsync(username);
}
var error = Uri.EscapeDataString(exception.Message);
return Results.Redirect($"/setup?error={error}");
}
catch
{
if (adminCreated)
{
await userService.DeleteUserByUsernameAsync(username);
}
return Results.Redirect("/setup?error=setup-failed");
}
return Results.Redirect("/login");
});
app.MapPost("/guests/add", async (
HttpContext httpContext,
GuestEntryService guestEntryService) =>
{
var form = await httpContext.Request.ReadFormAsync();
var name = form["name"].ToString();
var notes = form["notes"].ToString();
try
{
if (await guestEntryService.NameAndNotesExistAsync(name, notes))
{
return Results.Redirect("/guests?error=duplicate");
}
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
var username = httpContext.User.Identity?.Name ?? "unknown";
await guestEntryService.AddAsync(name, notes, userId, username);
}
catch (InvalidOperationException exception)
{
var error = Uri.EscapeDataString(exception.Message);
return Results.Redirect($"/guests?error={error}");
}
catch
{
return Results.Redirect("/guests?error=add-failed");
}
return Results.Redirect("/guests");
}).RequireAuthorization("StaffOnly");
app.MapPost("/guests/delete", async (
HttpContext httpContext,
GuestEntryService guestEntryService) =>
{
var form = await httpContext.Request.ReadFormAsync();
var id = form["id"].ToString();
if (string.IsNullOrWhiteSpace(id))
{
return Results.Redirect("/guests?error=Guest entry is missing an id.");
}
try
{
await guestEntryService.DeleteAsync(id);
}
catch
{
return Results.Redirect("/guests?error=Guest could not be deleted.");
}
return Results.Redirect("/guests");
}).RequireAuthorization("StaffOnly");
app.MapPost("/guests/update", async (
HttpContext httpContext,
GuestEntryService guestEntryService) =>
{
var form = await httpContext.Request.ReadFormAsync();
var id = form["id"].ToString();
var name = form["name"].ToString();
var notes = form["notes"].ToString();
if (string.IsNullOrWhiteSpace(id))
{
return Results.Redirect("/guests?error=No guest entry is selected for editing.");
}
try
{
if (await guestEntryService.NameAndNotesExistAsync(name, notes, id))
{
return Results.Redirect($"/guests?edit={Uri.EscapeDataString(id)}&error=duplicate");
}
await guestEntryService.UpdateAsync(id, name, notes);
}
catch (InvalidOperationException exception)
{
var error = Uri.EscapeDataString(exception.Message);
return Results.Redirect($"/guests?edit={Uri.EscapeDataString(id)}&error={error}");
}
catch
{
return Results.Redirect($"/guests?edit={Uri.EscapeDataString(id)}&error=Guest could not be updated.");
}
return Results.Redirect("/guests");
}).RequireAuthorization("StaffOnly");
app.MapPost("/admin/guest-access-code", async (
HttpContext httpContext,
AccessCodeService accessCodeService) =>
{
var form = await httpContext.Request.ReadFormAsync();
var accessCode = form["guestAccessCode"].ToString();
try
{
await accessCodeService.SetGuestAccessCodeAsync(accessCode);
}
catch (InvalidOperationException exception)
{
var error = Uri.EscapeDataString(exception.Message);
return Results.Redirect($"/admin?accessCodeMessage={error}");
}
catch
{
return Results.Redirect("/admin?accessCodeMessage=Guest access code could not be updated.");
}
return Results.Redirect("/admin?accessCodeMessage=Guest access code has been updated.");
}).RequireAuthorization("AdminOnly");
app.MapPost("/admin/maintainers/create", async (
HttpContext httpContext,
UserService userService) =>
{
var form = await httpContext.Request.ReadFormAsync();
var username = form["username"].ToString();
var displayName = form["displayName"].ToString();
var password = form["password"].ToString();
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = username;
}
try
{
await userService.CreateMaintainerAsync(displayName, username, password);
}
catch (InvalidOperationException exception)
{
var error = Uri.EscapeDataString(exception.Message);
return Results.Redirect($"/admin?maintainerMessage={error}");
}
catch
{
return Results.Redirect("/admin?maintainerMessage=Maintainer could not be created.");
}
return Results.Redirect("/admin?maintainerMessage=Maintainer has been created.");
}).RequireAuthorization("AdminOnly");
app.MapPost("/admin/maintainers/delete", async (
HttpContext httpContext,
UserService userService) =>
{
var form = await httpContext.Request.ReadFormAsync();
var id = form["id"].ToString();
if (string.IsNullOrWhiteSpace(id))
{
return Results.Redirect("/admin?maintainerMessage=Maintainer is missing an id.");
}
try
{
await userService.DeleteMaintainerAsync(id);
}
catch
{
return Results.Redirect("/admin?maintainerMessage=Maintainer could not be deleted.");
}
return Results.Redirect("/admin?maintainerMessage=Maintainer has been deleted.");
}).RequireAuthorization("AdminOnly");
app.MapPost("/admin/reset-setup", async (
UserService userService,
SetupStatusService setupStatusService) =>
{
try
{
await userService.BackupAndClearUsersAsync("Setup reset from admin panel");
await setupStatusService.ResetSetupAsync();
}
catch
{
return Results.Redirect("/admin?message=Setup could not be reset.");
}
return Results.Redirect("/admin?message=Setup has been reset. Existing users were moved to backup.");
}).RequireAuthorization("AdminOnly");
app.MapPost("/admin/setup-recovery", async (
AccessCodeService accessCodeService,
SetupStatusService setupStatusService) =>
{
try
{
if (!await accessCodeService.HasGuestAccessCodeAsync())
{
return Results.Redirect("/admin?recoveryMessage=Set a guest access code before completing recovery.");
}
await setupStatusService.MarkSetupCompleteAsync();
}
catch
{
return Results.Redirect("/admin?recoveryMessage=Setup recovery could not be completed.");
}
return Results.Redirect("/admin?recoveryMessage=Setup recovery has been completed.");
}).RequireAuthorization("AdminOnly");
using (var scope = app.Services.CreateScope())
{
var userService = scope.ServiceProvider.GetRequiredService<UserService>();
@@ -209,6 +479,7 @@ app.Use(async (context, next) =>
var allowedPaths =
isStaticFile ||
path.StartsWithSegments("/setup") ||
path.StartsWithSegments("/setup/complete") ||
path.StartsWithSegments("/auth/logout") ||
path.StartsWithSegments("/guest/logout") ||
path.StartsWithSegments("/_framework") ||