345 lines
12 KiB
Plaintext
345 lines
12 KiB
Plaintext
@page "/users"
|
|
@implements IDisposable
|
|
|
|
<PageTitle>Users Management - Chrono DB</PageTitle>
|
|
|
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
|
<div>
|
|
<h2 class="fw-bold mb-1 text-white">Users Database (`User`)</h2>
|
|
<p class="text-secondary mb-0">Browse, add, edit credentials, and delete user accounts.</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-chrono-secondary" @onclick="LoadUsersAsync">
|
|
<i class="bi bi-arrow-clockwise"></i> Refresh
|
|
</button>
|
|
<button class="btn btn-chrono-primary" @onclick="OpenAddModal">
|
|
<i class="bi bi-plus-lg"></i> Add New User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(alertMessage))
|
|
{
|
|
<div class="alert @(alertIsSuccess ? "alert-success" : "alert-danger") alert-dismissible fade show" role="alert">
|
|
<i class="bi @(alertIsSuccess ? "bi-check-circle-fill" : "bi-exclamation-triangle-fill") me-2"></i>
|
|
@alertMessage
|
|
<button type="button" class="btn-close" @onclick="() => alertMessage = null"></button>
|
|
</div>
|
|
}
|
|
|
|
<div class="chrono-card">
|
|
<div class="d-flex align-items-center justify-content-between mb-3 gap-3">
|
|
<div class="input-group" style="max-width: 400px;">
|
|
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
|
|
<input type="text" class="form-control form-control-chrono" placeholder="Search by username..."
|
|
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterUsers"/>
|
|
@if (!string.IsNullOrEmpty(searchTerm))
|
|
{
|
|
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
|
}
|
|
</div>
|
|
|
|
<div class="text-secondary small">
|
|
Showing @users.Count user(s)
|
|
</div>
|
|
</div>
|
|
|
|
@if (isLoading)
|
|
{
|
|
<div class="text-center py-5 text-secondary">
|
|
<div class="spinner-border text-primary mb-2" role="status"></div>
|
|
<div>Loading users from database...</div>
|
|
</div>
|
|
}
|
|
else if (!users.Any())
|
|
{
|
|
<div class="text-center py-5 text-secondary">
|
|
<i class="bi bi-people fs-1 d-block mb-2"></i>
|
|
<h5>No Users Found</h5>
|
|
<p class="small">There are currently no users in the database.</p>
|
|
<div class="d-flex justify-content-center gap-2 mt-3">
|
|
<button class="btn btn-chrono-primary" @onclick="OpenAddModal">
|
|
<i class="bi bi-plus-lg"></i> Add First User
|
|
</button>
|
|
<button class="btn btn-chrono-secondary" @onclick="SeedUsersAsync">
|
|
<i class="bi bi-box-arrow-in-down"></i> Seed Sample Users
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="chrono-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 30%;">Username (Primary Key)</th>
|
|
<th style="width: 35%;">Password Hash / Credential</th>
|
|
<th style="width: 20%;">Created At</th>
|
|
<th style="width: 140px;" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var user in users)
|
|
{
|
|
<tr>
|
|
<td>
|
|
<div class="fw-semibold text-white d-flex align-items-center gap-2">
|
|
<i class="bi bi-person-circle text-info"></i>
|
|
@user.Username
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="text-secondary font-monospace">••••••••</div>
|
|
</td>
|
|
<td>
|
|
<div class="text-secondary small">@user.CreatedAt.ToString("g")</div>
|
|
</td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(user)"
|
|
title="Edit Password">
|
|
<i class="bi bi-pencil-fill"></i>
|
|
</button>
|
|
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(user)"
|
|
title="Delete User">
|
|
<i class="bi bi-trash3-fill"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (showEditModal)
|
|
{
|
|
<div class="modal-backdrop fade show"></div>
|
|
<div class="modal fade show d-block" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content chrono-modal">
|
|
<div class="modal-header border-secondary border-opacity-25">
|
|
<h5 class="modal-title text-white">
|
|
<i class="bi @(isNewUser ? "bi-person-plus-fill" : "bi-pencil-square") me-2 text-primary"></i>
|
|
@(isNewUser ? "Add New User" : $"Edit User: {currentUser.Username}")
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label text-secondary small fw-semibold">USERNAME</label>
|
|
<input type="text" class="form-control form-control-chrono"
|
|
@bind="currentUser.Username" disabled="@(!isNewUser)"
|
|
placeholder="e.g. player1"/>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label text-secondary small fw-semibold">PASSWORD</label>
|
|
<input type="password" class="form-control form-control-chrono"
|
|
@bind="currentUser.Password"
|
|
placeholder="Enter password..."/>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-secondary border-opacity-25">
|
|
<button type="button" class="btn btn-chrono-secondary" @onclick="CloseModal">Cancel</button>
|
|
<button type="button" class="btn btn-chrono-primary" @onclick="SaveUserAsync" disabled="@isSaving">
|
|
<i class="bi bi-save me-1"></i> @(isSaving ? "Saving..." : "Save User")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (showDeleteModal && userToDelete != null)
|
|
{
|
|
<div class="modal-backdrop fade show"></div>
|
|
<div class="modal fade show d-block" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content chrono-modal">
|
|
<div class="modal-header border-secondary border-opacity-25">
|
|
<h5 class="modal-title text-danger">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i> Confirm Delete
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white"
|
|
@onclick="() => showDeleteModal = false"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-light mb-0">
|
|
Are you sure you want to delete user <strong class="text-white">@userToDelete.Username</strong>?
|
|
</p>
|
|
</div>
|
|
<div class="modal-footer border-secondary border-opacity-25">
|
|
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">
|
|
Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-chrono-danger" @onclick="ConfirmDeleteAsync"
|
|
disabled="@isSaving">
|
|
<i class="bi bi-trash3-fill me-1"></i> @(isSaving ? "Deleting..." : "Delete")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private List<User> users = [];
|
|
private bool isLoading = true;
|
|
private bool isSaving;
|
|
private string searchTerm = string.Empty;
|
|
|
|
private bool showEditModal;
|
|
private bool isNewUser;
|
|
private User currentUser = new();
|
|
|
|
private bool showDeleteModal;
|
|
private User? userToDelete;
|
|
|
|
private string? alertMessage;
|
|
private bool alertIsSuccess;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
|
await LoadUsersAsync();
|
|
}
|
|
|
|
private async void HandleConfigChanged()
|
|
{
|
|
await InvokeAsync(LoadUsersAsync);
|
|
}
|
|
|
|
private async Task LoadUsersAsync()
|
|
{
|
|
isLoading = true;
|
|
alertMessage = null;
|
|
try
|
|
{
|
|
users = await DbService.GetUsersAsync(searchTerm);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
alertMessage = $"Error loading users: {ex.Message}";
|
|
alertIsSuccess = false;
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task FilterUsers()
|
|
{
|
|
users = await DbService.GetUsersAsync(searchTerm);
|
|
}
|
|
|
|
private async Task ClearSearch()
|
|
{
|
|
searchTerm = string.Empty;
|
|
await LoadUsersAsync();
|
|
}
|
|
|
|
private void OpenAddModal()
|
|
{
|
|
currentUser = new User();
|
|
isNewUser = true;
|
|
showEditModal = true;
|
|
}
|
|
|
|
private void OpenEditModal(User user)
|
|
{
|
|
currentUser = new User
|
|
{
|
|
Username = user.Username,
|
|
Password = user.Password,
|
|
CreatedAt = user.CreatedAt
|
|
};
|
|
isNewUser = false;
|
|
showEditModal = true;
|
|
}
|
|
|
|
private void CloseModal()
|
|
{
|
|
showEditModal = false;
|
|
}
|
|
|
|
private async Task SaveUserAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(currentUser.Username) || string.IsNullOrWhiteSpace(currentUser.Password))
|
|
{
|
|
alertMessage = "Username and password cannot be empty.";
|
|
alertIsSuccess = false;
|
|
return;
|
|
}
|
|
|
|
isSaving = true;
|
|
try
|
|
{
|
|
var result = await DbService.SaveUserAsync(currentUser, isNewUser);
|
|
alertMessage = result.Message;
|
|
alertIsSuccess = result.Success;
|
|
if (result.Success)
|
|
{
|
|
showEditModal = false;
|
|
await LoadUsersAsync();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
isSaving = false;
|
|
}
|
|
}
|
|
|
|
private void OpenDeleteModal(User user)
|
|
{
|
|
userToDelete = user;
|
|
showDeleteModal = true;
|
|
}
|
|
|
|
private async Task ConfirmDeleteAsync()
|
|
{
|
|
if (userToDelete == null) return;
|
|
|
|
isSaving = true;
|
|
try
|
|
{
|
|
var result = await DbService.DeleteUserAsync(userToDelete.Username);
|
|
alertMessage = result.Message;
|
|
alertIsSuccess = result.Success;
|
|
showDeleteModal = false;
|
|
userToDelete = null;
|
|
await LoadUsersAsync();
|
|
}
|
|
finally
|
|
{
|
|
isSaving = false;
|
|
}
|
|
}
|
|
|
|
private async Task SeedUsersAsync()
|
|
{
|
|
isLoading = true;
|
|
try
|
|
{
|
|
var res = await DbService.SeedSampleDataAsync();
|
|
alertMessage = res.Message;
|
|
alertIsSuccess = res.Success;
|
|
await LoadUsersAsync();
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
|
}
|
|
|
|
}
|