basic admin panel stuff

This commit is contained in:
Lapis
2026-06-17 13:39:28 -04:00
parent c20d215ee0
commit 004f1921a6
44 changed files with 3432 additions and 315 deletions
@@ -0,0 +1,144 @@
@page "/setup/accounts"
@attribute [Authorize(Policy = "OwnerOnly")]
@rendermode InteractiveServer
@using CannedNet.Admin.Models
@using CannedNet.Admin.Services
@inject AdminAuthService AuthService
<PageTitle>Admin Accounts</PageTitle>
<h3>Admin Accounts</h3>
<p class="text-muted">Manage admin panel accounts. Only the owner can create or delete accounts.</p>
<div class="card mb-4">
<div class="card-header">Create New Admin Account</div>
<div class="card-body">
<form>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">Username</label>
<input type="text" class="form-control" @bind="newUsername" required />
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Password</label>
<input type="password" class="form-control" @bind="newPassword" required minlength="6" />
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Confirm Password</label>
<input type="password" class="form-control" @bind="newConfirmPassword" required />
</div>
</div>
<button type="submit" class="btn btn-success" @onclick="CreateAccount" disabled="@creating">
@(creating ? "Creating..." : "Create Account")
</button>
@if (!string.IsNullOrEmpty(createMessage))
{
<span class="ms-2 @(createMessage.Contains("Error") ? "text-danger" : "text-success")">@createMessage</span>
}
</form>
</div>
</div>
<div class="card">
<div class="card-header">Admin Accounts</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var user in AuthService.GetAll())
{
<tr>
<td>@user.Id</td>
<td>@user.Username</td>
<td>
@if (user.IsOwner)
{
<td>Owner</td>
}
else
{
<td>Admin</td>
}
</td>
<td>@user.CreatedAt.ToString("g")</td>
<td>
@if (!user.IsOwner)
{
<button class="btn btn-sm btn-danger" @onclick="() => DeleteUser(user.Id)">Delete</button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@code {
private string newUsername = "";
private string newPassword = "";
private string newConfirmPassword = "";
private bool creating;
private string? createMessage;
private async Task CreateAccount()
{
creating = true;
createMessage = null;
try
{
if (string.IsNullOrWhiteSpace(newUsername))
{
createMessage = "Username is required.";
return;
}
if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
{
createMessage = "Password must be at least 6 characters.";
return;
}
if (newPassword != newConfirmPassword)
{
createMessage = "Passwords do not match.";
return;
}
if (AuthService.GetByUsername(newUsername) != null)
{
createMessage = "Username already exists.";
return;
}
AuthService.CreateUser(newUsername, newPassword, isOwner: false);
createMessage = "Admin account created successfully!";
newUsername = "";
newPassword = "";
newConfirmPassword = "";
}
catch (Exception ex)
{
createMessage = $"Error: {ex.Message}";
}
finally
{
creating = false;
}
}
private async Task DeleteUser(int id)
{
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm", "Delete this admin account?");
if (!confirmed) return;
AuthService.DeleteUser(id);
}
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
}
@@ -0,0 +1,110 @@
@page "/setup"
@rendermode InteractiveServer
@using CannedNet.Admin.Services
@inject AdminAuthService AuthService
@inject NavigationManager Navigation
<PageTitle>First-Time Setup</PageTitle>
<div class="row justify-content-center mt-5">
<div class="col-md-6">
<div class="card">
<div class="card-header"><h4 class="mb-0">First-Time Setup</h4></div>
<div class="card-body">
@if (completed)
{
<div class="alert alert-success">
<h5>Owner account created!</h5>
<p>You can now <a href="/login">log in</a> with your new admin account.</p>
</div>
}
else if (AuthService.HasAnyUser())
{
<div class="alert alert-info">
Admin accounts already exist. <a href="/login">Go to login</a>.
</div>
}
else
{
<p class="text-muted">Create the initial owner account. This account will have full access and can create additional admin accounts.</p>
<form>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" class="form-control" @bind="username" required />
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" class="form-control" @bind="password" required minlength="6" />
</div>
<div class="mb-3">
<label class="form-label">Confirm Password</label>
<input type="password" class="form-control" @bind="confirmPassword" required />
</div>
<button type="submit" class="btn btn-primary w-100" @onclick="CreateOwner" disabled="@creating">
@(creating ? "Creating..." : "Create Owner Account")
</button>
@if (!string.IsNullOrEmpty(message))
{
<div class="alert mt-3 @(message.Contains("Error") ? "alert-danger" : "alert-success")">@message</div>
}
</form>
}
</div>
</div>
</div>
</div>
@code {
private string username = "";
private string password = "";
private string confirmPassword = "";
private bool creating;
private bool completed;
private string? message;
private async Task CreateOwner()
{
creating = true;
message = null;
try
{
if (string.IsNullOrWhiteSpace(username))
{
message = "Username is required.";
return;
}
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
{
message = "Password must be at least 6 characters.";
return;
}
if (password != confirmPassword)
{
message = "Passwords do not match.";
return;
}
if (AuthService.HasAnyUser())
{
message = "Admin accounts already exist.";
return;
}
if (AuthService.GetByUsername(username) != null)
{
message = "Username already taken.";
return;
}
AuthService.CreateUser(username, password, isOwner: true);
completed = true;
}
catch (Exception ex)
{
message = $"Error: {ex.Message}";
}
finally
{
creating = false;
}
}
}
@@ -0,0 +1,110 @@
@page "/setup/nameserver"
@rendermode InteractiveServer
@using CannedNet.Admin.Models
@using System.Text.Json
<PageTitle>Nameserver Setup</PageTitle>
<h3>Nameserver Endpoints</h3>
<p class="text-muted">Configure the base server URL the game client connects to. All endpoints are derived from this URL.</p>
@if (loading)
{
<div class="spinner-border" role="status"></div>
}
else if (!string.IsNullOrEmpty(error))
{
<div class="alert alert-danger">@error</div>
}
else
{
<form>
<div class="mb-3">
<button type="submit" class="btn btn-success" @onclick="Save" disabled="@saving">
@(saving ? "Saving..." : "Save Changes")
</button>
@if (!string.IsNullOrEmpty(saveMessage))
{
<span class="@(saveError ? "text-danger" : "text-success") ms-2">@saveMessage</span>
}
</div>
<div class="mb-3">
<label class="form-label" for="baseUrl">Base URL:</label>
<input id="baseUrl" class="form-control font-monospace" @bind="baseUrl" placeholder="https://cannednet.lapis.codes" />
<div class="form-text">All endpoint URLs will use this as the base.</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Service</th>
<th>Full URL</th>
</tr>
</thead>
<tbody>
@foreach (var (name, path) in EndpointDefinitions.Endpoints)
{
<tr>
<td><code>@name</code></td>
<td><code>@(baseUrl.TrimEnd('/') + path)</code></td>
</tr>
}
</tbody>
</table>
</div>
</form>
}
@code {
[Inject] private HttpClient Http { get; set; } = default!;
private bool loading = true;
private bool saving;
private bool saveError;
private string? error;
private string? saveMessage;
private string baseUrl = "";
protected override async Task OnInitializedAsync()
{
try
{
var json = await Http.GetStringAsync("/admin/api/endpoints");
baseUrl = EndpointDefinitions.ExtractBaseUrl(json);
}
catch (Exception ex)
{
error = $"Failed to load: {ex.Message}";
}
finally
{
loading = false;
}
}
private async Task Save()
{
saving = true;
saveMessage = null;
saveError = false;
try
{
var json = EndpointDefinitions.BuildEndpointsJson(baseUrl);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await Http.PutAsync("/admin/api/endpoints", content);
response.EnsureSuccessStatusCode();
saveMessage = "Saved successfully!";
}
catch (Exception ex)
{
saveMessage = $"Error: {ex.Message}";
saveError = true;
}
finally
{
saving = false;
}
}
}