API Test
This commit is contained in:
@@ -22,7 +22,8 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-chrono-secondary" @onclick="RefreshHealthAsync" title="Refresh Health Check">
|
||||
<button class="btn btn-sm btn-chrono-secondary" @onclick="RefreshHealthAsync"
|
||||
title="Refresh Health Check">
|
||||
<i class="bi bi-arrow-clockwise @(isChecking ? "spin" : "")"></i> Refresh
|
||||
</button>
|
||||
<a href="test-suite" class="btn btn-sm btn-chrono-primary">
|
||||
@@ -94,4 +95,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,20 +21,20 @@ main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
@@ -90,9 +90,9 @@ main {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
<div class="p-2 rounded bg-dark border border-secondary border-opacity-25">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<span class="text-secondary small fw-semibold">PROVIDER</span>
|
||||
<span class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")" style="font-size: 0.72rem;">
|
||||
<span
|
||||
class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")"
|
||||
style="font-size: 0.72rem;">
|
||||
@ConfigService.ProviderType
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-truncate text-light" style="font-size: 0.78rem;" title="@ConfigService.GetActiveProviderDescription()">
|
||||
<div class="small text-truncate text-light" style="font-size: 0.78rem;"
|
||||
title="@ConfigService.GetActiveProviderDescription()">
|
||||
@(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "PostgreSQL Server" : "In-Memory Test DB")
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,6 +38,11 @@
|
||||
<span>User Decks</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="nav-link" href="users">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
<span>Users</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="nav-link" href="passkeys">
|
||||
<i class="bi bi-key-fill"></i>
|
||||
<span>Passkeys</span>
|
||||
@@ -59,6 +67,7 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
||||
@@ -73,4 +82,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -51,33 +51,33 @@
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
background-color: rgba(255, 255, 255, 0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,11 @@
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
&[open] {
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,13 +95,13 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
@@ -110,17 +109,17 @@
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
<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 card name or note content..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterNotes" />
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
placeholder="Search by card name or note content..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterNotes"/>
|
||||
@if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
||||
@@ -72,37 +73,44 @@
|
||||
<div class="table-responsive">
|
||||
<table class="chrono-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%;">Card Name (Primary Key)</th>
|
||||
<th>Note Content</th>
|
||||
<th style="width: 140px;" class="text-end">Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width: 20%;">User</th>
|
||||
<th style="width: 25%;">Card Name</th>
|
||||
<th>Note Content</th>
|
||||
<th style="width: 140px;" class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var note in notes)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold text-white d-flex align-items-center gap-2">
|
||||
<i class="bi bi-card-heading text-primary"></i>
|
||||
@note.CardName
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-light text-break">@note.Note</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(note)" title="Edit Note">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(note)" title="Delete Note">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var note in notes)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
class="badge bg-secondary font-monospace">@(string.IsNullOrEmpty(note.Username) ? "global" : note.Username)</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-white d-flex align-items-center gap-2">
|
||||
<i class="bi bi-card-heading text-primary"></i>
|
||||
@note.CardName
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-light text-break">@note.Note</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(note)"
|
||||
title="Edit Note">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(note)"
|
||||
title="Delete Note">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -122,24 +130,44 @@
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal"></button>
|
||||
</div>
|
||||
<div class="modal-body-custom">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">USERNAME</label>
|
||||
@if (isEditing)
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono" value="@currentNote.Username"
|
||||
disabled/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
placeholder="Enter username (e.g. player1)..." @bind="currentNote.Username"/>
|
||||
<div class="form-text text-secondary">The username this note is associated with.</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">CARD NAME</label>
|
||||
@if (isEditing)
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono" value="@currentNote.CardName" disabled />
|
||||
<div class="form-text text-secondary">Card name acts as the primary key and cannot be altered.</div>
|
||||
<input type="text" class="form-control form-control-chrono" value="@currentNote.CardName"
|
||||
disabled/>
|
||||
<div class="form-text text-secondary">Card name acts as part of the primary key and cannot be
|
||||
altered.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono" list="cardSuggestions"
|
||||
placeholder="Enter or select card name..." @bind="currentNote.CardName" />
|
||||
placeholder="Enter or select card name..." @bind="currentNote.CardName"/>
|
||||
<datalist id="cardSuggestions">
|
||||
@foreach (var c in availableCardNames.Take(50))
|
||||
{
|
||||
<option value="@c" />
|
||||
<option value="@c"/>
|
||||
}
|
||||
</datalist>
|
||||
<div class="form-text text-secondary">Select an existing card from the database or enter a custom name.</div>
|
||||
<div class="form-text text-secondary">Select an existing card from the database or enter a
|
||||
custom name.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -170,17 +198,20 @@
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
Confirm Delete Note
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="() => showDeleteModal = false"></button>
|
||||
<button type="button" class="btn-close btn-close-white"
|
||||
@onclick="() => showDeleteModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body-custom">
|
||||
<p class="text-white mb-2">Are you sure you want to delete the note for:</p>
|
||||
<div class="p-2 rounded bg-dark border border-secondary fw-bold text-primary mb-3">
|
||||
@noteToDelete.CardName
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">This action will remove the record from the database permanently.</p>
|
||||
<p class="text-secondary small mb-0">This action will remove the record from the database
|
||||
permanently.</p>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel</button>
|
||||
<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">
|
||||
@(isSaving ? "Deleting..." : "Delete Permanently")
|
||||
</button>
|
||||
@@ -194,7 +225,7 @@
|
||||
private List<string> availableCardNames = [];
|
||||
private string searchTerm = "";
|
||||
private bool isLoading = true;
|
||||
private bool isSaving = false;
|
||||
private bool isSaving;
|
||||
private string? alertMessage;
|
||||
private bool alertIsSuccess;
|
||||
|
||||
@@ -208,7 +239,7 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
availableCardNames = CardDatabase.Cards
|
||||
@@ -272,6 +303,7 @@
|
||||
isEditing = true;
|
||||
currentNote = new CardNote
|
||||
{
|
||||
Username = note.Username,
|
||||
CardName = note.CardName,
|
||||
Note = note.Note
|
||||
};
|
||||
@@ -317,7 +349,7 @@
|
||||
if (noteToDelete == null) return;
|
||||
|
||||
isSaving = true;
|
||||
var result = await DbService.DeleteCardNoteAsync(noteToDelete.CardName);
|
||||
var result = await DbService.DeleteCardNoteAsync(noteToDelete.CardName, noteToDelete.Username);
|
||||
isSaving = false;
|
||||
|
||||
alertIsSuccess = result.Success;
|
||||
@@ -340,4 +372,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
private int currentCount;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Database Settings & Migrations</h2>
|
||||
<p class="text-secondary mb-0">Configure database providers, update connection strings, inspect EF Core migrations, and run maintenance tasks.</p>
|
||||
<p class="text-secondary mb-0">Configure database providers, update connection strings, inspect EF Core
|
||||
migrations, and run maintenance tasks.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="RefreshAsync">
|
||||
@@ -38,12 +39,14 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">ACTIVE PROVIDER</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn flex-fill @(selectedProvider == DatabaseProviderType.PostgreSQL ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
@onclick="() => SelectProvider(DatabaseProviderType.PostgreSQL)">
|
||||
<button
|
||||
class="btn flex-fill @(selectedProvider == DatabaseProviderType.PostgreSQL ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
@onclick="() => SelectProvider(DatabaseProviderType.PostgreSQL)">
|
||||
<i class="bi bi-database me-1"></i> PostgreSQL (Cloud DB)
|
||||
</button>
|
||||
<button class="btn flex-fill @(selectedProvider == DatabaseProviderType.InMemory ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
@onclick="() => SelectProvider(DatabaseProviderType.InMemory)">
|
||||
<button
|
||||
class="btn flex-fill @(selectedProvider == DatabaseProviderType.InMemory ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
@onclick="() => SelectProvider(DatabaseProviderType.InMemory)">
|
||||
<i class="bi bi-cpu me-1"></i> In-Memory Test DB
|
||||
</button>
|
||||
</div>
|
||||
@@ -80,7 +83,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">IN-MEMORY DATABASE INSTANCE NAME</label>
|
||||
<input type="text" class="form-control form-control-chrono font-monospace"
|
||||
@bind="inMemoryNameInput" />
|
||||
@bind="inMemoryNameInput"/>
|
||||
<div class="form-text text-secondary">
|
||||
In-memory EF Core provider creates an isolated database in RAM for instant testing.
|
||||
</div>
|
||||
@@ -112,29 +115,34 @@
|
||||
@if (health != null)
|
||||
{
|
||||
<div class="d-flex flex-column gap-2 mb-4">
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">Connection Status</span>
|
||||
<span class="fw-bold @(health.IsConnected ? "text-success" : "text-danger")">
|
||||
@(health.IsConnected ? "Connected" : "Disconnected")
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">Query Latency</span>
|
||||
<span class="text-white font-monospace">@(health.ResponseTimeMs) ms</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">CardNotes Table Rows</span>
|
||||
<span class="text-primary fw-bold">@health.CardNotesCount</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">UserDecks Table Rows</span>
|
||||
<span class="text-success fw-bold">@health.UserDecksCount</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">PasskeyCredentials Table Rows</span>
|
||||
<span class="text-purple fw-bold">@health.PasskeysCount</span>
|
||||
</div>
|
||||
@@ -162,7 +170,8 @@
|
||||
@if (migrationReport?.SupportsMigrations == true && migrationReport.PendingMigrations.Any())
|
||||
{
|
||||
<button class="btn btn-chrono-primary btn-sm" @onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
|
||||
<i class="bi bi-arrow-up-circle-fill me-1"></i> Apply Pending Migrations (@migrationReport.PendingMigrations.Count)
|
||||
<i class="bi bi-arrow-up-circle-fill me-1"></i> Apply Pending Migrations
|
||||
(@migrationReport.PendingMigrations.Count)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -170,14 +179,16 @@
|
||||
@if (migrationReport == null)
|
||||
{
|
||||
<div class="text-center py-4 text-secondary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div> Loading migration report...
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
Loading migration report...
|
||||
</div>
|
||||
}
|
||||
else if (!migrationReport.SupportsMigrations)
|
||||
{
|
||||
<div class="p-3 bg-dark rounded border border-secondary text-secondary small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Migrations are managed via relational schema (PostgreSQL). The active in-memory provider does not use relational migration tables.
|
||||
Migrations are managed via relational schema (PostgreSQL). The active in-memory provider does not use
|
||||
relational migration tables.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -200,7 +211,8 @@
|
||||
<div class="d-flex flex-column gap-2" style="max-height: 250px; overflow-y: auto;">
|
||||
@foreach (var m in migrationReport.AppliedMigrations)
|
||||
{
|
||||
<div class="p-2 rounded bg-dark border border-success border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-success border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<span class="text-white small font-monospace">@m</span>
|
||||
<span class="badge bg-success"><i class="bi bi-check"></i> Applied</span>
|
||||
</div>
|
||||
@@ -218,7 +230,8 @@
|
||||
@if (!migrationReport.PendingMigrations.Any())
|
||||
{
|
||||
<div class="p-3 bg-dark rounded border border-success border-opacity-25 text-success small">
|
||||
<i class="bi bi-shield-check me-1"></i> All migrations are applied. Schema is in sync with AppDbContext.
|
||||
<i class="bi bi-shield-check me-1"></i> All migrations are applied. Schema is in sync with
|
||||
AppDbContext.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -226,7 +239,8 @@
|
||||
<div class="d-flex flex-column gap-2" style="max-height: 250px; overflow-y: auto;">
|
||||
@foreach (var m in migrationReport.PendingMigrations)
|
||||
{
|
||||
<div class="p-2 rounded bg-dark border border-warning border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-warning border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<span class="text-warning small font-monospace">@m</span>
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Pending</span>
|
||||
</div>
|
||||
@@ -357,4 +371,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that
|
||||
occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong>
|
||||
environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@@ -30,7 +32,9 @@
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,8 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Database Test Dashboard</h2>
|
||||
<p class="text-secondary mb-0">Monitor, manage, and validate all database features referenced in Chrono Cloud</p>
|
||||
<p class="text-secondary mb-0">Monitor, manage, and validate all database features referenced in Chrono
|
||||
Cloud</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="RefreshAsync" disabled="@isLoading">
|
||||
@@ -78,49 +79,66 @@
|
||||
|
||||
<!-- Metrics Row -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="card-notes" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-blue">
|
||||
<i class="bi bi-card-text"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.CardNotesCount.ToString() : "-")</div>
|
||||
<div
|
||||
class="metric-value">@(health?.IsConnected == true ? health.CardNotesCount.ToString() : "-")</div>
|
||||
<div class="metric-label">Card Notes</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="users" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-yellow">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.UsersCount.ToString() : "-")</div>
|
||||
<div class="metric-label">Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md">
|
||||
<a href="user-decks" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-green">
|
||||
<i class="bi bi-collection-play"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.UserDecksCount.ToString() : "-")</div>
|
||||
<div
|
||||
class="metric-value">@(health?.IsConnected == true ? health.UserDecksCount.ToString() : "-")</div>
|
||||
<div class="metric-label">User Decks</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="passkeys" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-purple">
|
||||
<i class="bi bi-key-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.PasskeysCount.ToString() : "-")</div>
|
||||
<div
|
||||
class="metric-value">@(health?.IsConnected == true ? health.PasskeysCount.ToString() : "-")</div>
|
||||
<div class="metric-label">Passkeys</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="diagnostics" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-yellow">
|
||||
@@ -128,11 +146,11 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">
|
||||
@(health?.ProviderType == DatabaseProviderType.PostgreSQL
|
||||
? $"{health.AppliedMigrationsCount} / {health.AppliedMigrationsCount + health.PendingMigrationsCount}"
|
||||
@(health?.ProviderType == DatabaseProviderType.PostgreSQL
|
||||
? $"{health.AppliedMigrationsCount} / {health.AppliedMigrationsCount + health.PendingMigrationsCount}"
|
||||
: "N/A")
|
||||
</div>
|
||||
<div class="metric-label">Applied Migrations</div>
|
||||
<div class="metric-label">Migrations</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -158,7 +176,8 @@
|
||||
<a href="card-notes" class="btn btn-sm btn-chrono-secondary">Manage Notes</a>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">
|
||||
Tracks developer and user strategic card notes with primary key indexing on <code>CardName</code>. Supports live search, adding, updating, and deletion.
|
||||
Tracks developer and user strategic card notes with primary key indexing on
|
||||
<code>CardName</code>. Supports live search, adding, updating, and deletion.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +187,8 @@
|
||||
<a href="user-decks" class="btn btn-sm btn-chrono-secondary">Manage Decks</a>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">
|
||||
Stores full player decks with native PostgreSQL <code>jsonb</code> mapping for <code>Cards</code> list (with counts) and <code>Divers</code> list.
|
||||
Stores full player decks with native PostgreSQL <code>jsonb</code> mapping for
|
||||
<code>Cards</code> list (with counts) and <code>Divers</code> list.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +198,8 @@
|
||||
<a href="passkeys" class="btn btn-sm btn-chrono-secondary">Manage Passkeys</a>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">
|
||||
Stores WebAuthn / FIDO2 binary credentials (raw <code>byte[]</code> for CredentialId, PublicKey, UserHandle) with counter verification.
|
||||
Stores WebAuthn / FIDO2 binary credentials (raw <code>byte[]</code> for CredentialId, PublicKey,
|
||||
UserHandle) with counter verification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,37 +216,47 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<button class="btn btn-chrono-primary py-2 text-start d-flex align-items-center justify-content-between" @onclick="SeedDataAsync" disabled="@isActionBusy">
|
||||
<button class="btn btn-chrono-primary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="SeedDataAsync" disabled="@isActionBusy">
|
||||
<span><i class="bi bi-box-arrow-in-down me-2"></i> Seed Realistic Sample Data</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between" @onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
|
||||
<button
|
||||
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
|
||||
<span><i class="bi bi-database-check me-2"></i> Apply Pending Migrations</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
@if (ConfigService.ProviderType == DatabaseProviderType.PostgreSQL)
|
||||
{
|
||||
<button class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between" @onclick="SwitchToInMemoryAsync">
|
||||
<button
|
||||
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="SwitchToInMemoryAsync">
|
||||
<span><i class="bi bi-cpu me-2"></i> Switch to In-Memory DB</span>
|
||||
<span class="badge bg-purple">Instant</span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between" @onclick="SwitchToPostgresAsync">
|
||||
<button
|
||||
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="SwitchToPostgresAsync">
|
||||
<span><i class="bi bi-database me-2"></i> Switch to PostgreSQL</span>
|
||||
<span class="badge bg-primary">Live</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<a href="test-suite" class="btn btn-chrono-outline-primary py-2 text-start d-flex align-items-center justify-content-between">
|
||||
<a href="test-suite"
|
||||
class="btn btn-chrono-outline-primary py-2 text-start d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-clipboard2-check me-2"></i> Open Test Suite Runner</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
|
||||
<button class="btn btn-chrono-danger py-2 text-start d-flex align-items-center justify-content-between mt-2" @onclick="ResetDatabaseAsync" disabled="@isActionBusy">
|
||||
<button
|
||||
class="btn btn-chrono-danger py-2 text-start d-flex align-items-center justify-content-between mt-2"
|
||||
@onclick="ResetDatabaseAsync" disabled="@isActionBusy">
|
||||
<span><i class="bi bi-trash3-fill me-2"></i> Wipe & Reset Database</span>
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</button>
|
||||
@@ -310,4 +341,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
@page "/passkeys"
|
||||
@implements IDisposable
|
||||
@using System.Security.Cryptography
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>Passkeys 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">Passkey Credentials Database (`PasskeyCredential`)</h2>
|
||||
<p class="text-secondary mb-0">Browse, add, edit, and delete WebAuthn / FIDO2 security credentials with raw binary byte arrays.</p>
|
||||
<p class="text-secondary mb-0">Browse, add, edit, and delete WebAuthn / FIDO2 security credentials with raw
|
||||
binary byte arrays.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="LoadPasskeysAsync">
|
||||
@@ -32,8 +33,9 @@
|
||||
<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 AaGuid, ID, or SignCount..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterPasskeys" />
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
placeholder="Search by AaGuid, ID, or SignCount..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterPasskeys"/>
|
||||
@if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
||||
@@ -73,53 +75,61 @@
|
||||
<div class="table-responsive">
|
||||
<table class="chrono-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">ID</th>
|
||||
<th style="width: 220px;">AAGUID</th>
|
||||
<th style="width: 120px;">Sign Count</th>
|
||||
<th>Credential ID (Byte[])</th>
|
||||
<th>Public Key (Byte[])</th>
|
||||
<th style="width: 170px;" class="text-end">Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width: 80px;">ID</th>
|
||||
<th style="width: 220px;">AAGUID</th>
|
||||
<th style="width: 120px;">Sign Count</th>
|
||||
<th>Credential ID (Byte[])</th>
|
||||
<th>Public Key (Byte[])</th>
|
||||
<th style="width: 170px;" class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var p in passkeys)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge bg-dark border border-secondary text-white">#@p.Id</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-white small font-monospace">@(string.IsNullOrEmpty(p.AaGuid) ? "(None)" : p.AaGuid)</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-purple">@p.SignCount</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;" title="@Convert.ToHexString(p.CredentialId)">
|
||||
@Convert.ToHexString(p.CredentialId)[..Math.Min(16, p.CredentialId.Length * 2)]... (@p.CredentialId.Length bytes)
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;" title="@Convert.ToHexString(p.PublicKey)">
|
||||
@Convert.ToHexString(p.PublicKey)[..Math.Min(16, p.PublicKey.Length * 2)]... (@p.PublicKey.Length bytes)
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenViewModal(p)" title="View Binary Details">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(p)" title="Edit Passkey">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(p)" title="Delete Passkey">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var p in passkeys)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge bg-dark border border-secondary text-white">#@p.Id</span>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
class="text-white small font-monospace">@(string.IsNullOrEmpty(p.AaGuid) ? "(None)" : p.AaGuid)</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-purple">@p.SignCount</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;"
|
||||
title="@Convert.ToHexString(p.CredentialId)">
|
||||
@Convert.ToHexString(p.CredentialId)[..Math.Min(16, p.CredentialId.Length * 2)]...
|
||||
(@p.CredentialId.Length bytes)
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;"
|
||||
title="@Convert.ToHexString(p.PublicKey)">
|
||||
@Convert.ToHexString(p.PublicKey)[..Math.Min(16, p.PublicKey.Length * 2)]...
|
||||
(@p.PublicKey.Length bytes)
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenViewModal(p)"
|
||||
title="View Binary Details">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(p)"
|
||||
title="Edit Passkey">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(p)"
|
||||
title="Delete Passkey">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -151,13 +161,14 @@
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label text-secondary small fw-bold">AAGUID</label>
|
||||
<input type="text" class="form-control form-control-chrono font-monospace" placeholder="e.g. 00000000-0000-0000-0000-000000000000"
|
||||
@bind="currentPasskey.AaGuid" />
|
||||
<input type="text" class="form-control form-control-chrono font-monospace"
|
||||
placeholder="e.g. 00000000-0000-0000-0000-000000000000"
|
||||
@bind="currentPasskey.AaGuid"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary small fw-bold">SIGN COUNT</label>
|
||||
<input type="number" class="form-control form-control-chrono" min="0"
|
||||
@bind="currentPasskey.SignCount" />
|
||||
@bind="currentPasskey.SignCount"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +199,7 @@
|
||||
</div>
|
||||
<input type="text" class="form-control form-control-chrono font-monospace"
|
||||
placeholder="e.g. DEADBEAF01020304"
|
||||
@bind="userHandleHex" />
|
||||
@bind="userHandleHex"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
@@ -254,7 +265,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showViewModal = false">Close</button>
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showViewModal = false">Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,17 +282,20 @@
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
Confirm Delete Passkey
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="() => showDeleteModal = false"></button>
|
||||
<button type="button" class="btn-close btn-close-white"
|
||||
@onclick="() => showDeleteModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body-custom">
|
||||
<p class="text-white mb-2">Are you sure you want to delete passkey credential:</p>
|
||||
<div class="p-2 rounded bg-dark border border-secondary fw-bold text-primary mb-3 font-monospace">
|
||||
Passkey #@passkeyToDelete.Id (AAGUID: @(passkeyToDelete.AaGuid ?? "N/A"))
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">This will permanently delete the binary security credential from the database.</p>
|
||||
<p class="text-secondary small mb-0">This will permanently delete the binary security credential from
|
||||
the database.</p>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel</button>
|
||||
<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">
|
||||
@(isSaving ? "Deleting..." : "Delete Passkey")
|
||||
</button>
|
||||
@@ -293,18 +308,20 @@
|
||||
private List<PasskeyCredential> passkeys = [];
|
||||
private string searchTerm = "";
|
||||
private bool isLoading = true;
|
||||
private bool isSaving = false;
|
||||
private bool isSaving;
|
||||
private string? alertMessage;
|
||||
private bool alertIsSuccess;
|
||||
|
||||
private bool showModal;
|
||||
private bool isEditing;
|
||||
|
||||
private PasskeyCredential currentPasskey = new()
|
||||
{
|
||||
CredentialId = [],
|
||||
PublicKey = [],
|
||||
UserHandle = []
|
||||
};
|
||||
|
||||
private string credIdHex = "";
|
||||
private string pubKeyHex = "";
|
||||
private string userHandleHex = "";
|
||||
@@ -429,12 +446,14 @@
|
||||
alertMessage = "Invalid Credential ID hex string (must be even length).";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cleanPub) || cleanPub.Length % 2 != 0)
|
||||
{
|
||||
alertIsSuccess = false;
|
||||
alertMessage = "Invalid Public Key hex string (must be even length).";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cleanUser) || cleanUser.Length % 2 != 0)
|
||||
{
|
||||
alertIsSuccess = false;
|
||||
@@ -506,4 +525,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Database Feature Test Suite</h2>
|
||||
<p class="text-secondary mb-0">Automated end-to-end verification of all database features, constraints, and JSONB mappings referenced in Chrono Cloud.</p>
|
||||
<p class="text-secondary mb-0">Automated end-to-end verification of all database features, constraints, and
|
||||
JSONB mappings referenced in Chrono Cloud.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="ClearResults" disabled="@isRunning">
|
||||
@@ -47,7 +48,8 @@
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box @(testResults.Any(t => t.Status == TestStatus.Failed) ? "metric-icon-red" : "metric-icon-purple")">
|
||||
<div
|
||||
class="metric-icon-box @(testResults.Any(t => t.Status == TestStatus.Failed) ? "metric-icon-red" : "metric-icon-purple")">
|
||||
<i class="bi @(testResults.Any(t => t.Status == TestStatus.Failed) ? "bi-x-circle-fill text-danger" : "bi-shield-check")"></i>
|
||||
</div>
|
||||
<div>
|
||||
@@ -94,7 +96,8 @@
|
||||
</h5>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-secondary small">Target Provider:</span>
|
||||
<span class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")">
|
||||
<span
|
||||
class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")">
|
||||
@ConfigService.ProviderType
|
||||
</span>
|
||||
</div>
|
||||
@@ -121,7 +124,8 @@
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary font-monospace">@test.Id</span>
|
||||
<span class="fw-bold text-white fs-6">@test.Name</span>
|
||||
<span class="badge bg-dark border border-secondary text-secondary small">@test.Category</span>
|
||||
<span
|
||||
class="badge bg-dark border border-secondary text-secondary small">@test.Category</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
@@ -187,7 +191,7 @@
|
||||
|
||||
@code {
|
||||
private List<DatabaseTestCaseResult> testResults = [];
|
||||
private HashSet<string> expandedTestLogs = [];
|
||||
private readonly HashSet<string> expandedTestLogs = [];
|
||||
private bool isRunning;
|
||||
private string currentProgressStatus = "";
|
||||
|
||||
@@ -240,24 +244,31 @@
|
||||
expandedTestLogs.Add(testId);
|
||||
}
|
||||
|
||||
private static string GetBorderClass(TestStatus status) => status switch
|
||||
private static string GetBorderClass(TestStatus status)
|
||||
{
|
||||
TestStatus.Passed => "border-success border-opacity-50",
|
||||
TestStatus.Failed => "border-danger border-opacity-75",
|
||||
TestStatus.Running => "border-primary border-opacity-50",
|
||||
_ => "border-secondary border-opacity-25"
|
||||
};
|
||||
return status switch
|
||||
{
|
||||
TestStatus.Passed => "border-success border-opacity-50",
|
||||
TestStatus.Failed => "border-danger border-opacity-75",
|
||||
TestStatus.Running => "border-primary border-opacity-50",
|
||||
_ => "border-secondary border-opacity-25"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetBadgeClass(TestStatus status) => status switch
|
||||
private static string GetBadgeClass(TestStatus status)
|
||||
{
|
||||
TestStatus.Passed => "bg-success",
|
||||
TestStatus.Failed => "bg-danger",
|
||||
TestStatus.Running => "bg-primary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
return status switch
|
||||
{
|
||||
TestStatus.Passed => "bg-success",
|
||||
TestStatus.Failed => "bg-danger",
|
||||
TestStatus.Running => "bg-primary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,344 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
@using DatabaseTest.Components.Pages
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||
</Found>
|
||||
</Router>
|
||||
Reference in New Issue
Block a user