This commit is contained in:
6d486f49
2026-08-17 01:13:48 -04:00
parent 1d6207fbfe
commit ffaab59585
26 changed files with 3133 additions and 105 deletions
+2 -2
View File
@@ -12,11 +12,11 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"/>
<link rel="stylesheet" href="app.css"/>
<HeadOutlet/>
<HeadOutlet @rendermode="InteractiveServer"/>
</head>
<body>
<Routes/>
<Routes @rendermode="InteractiveServer"/>
<script src="_framework/blazor.web.js"></script>
</body>
@@ -117,69 +117,63 @@
@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 class="modal-backdrop-custom">
<div class="modal-dialog-custom">
<div class="modal-header-custom">
<h5 class="modal-title-custom">
<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-custom">
<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="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 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-custom">
<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>
}
@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 class="modal-backdrop-custom">
<div class="modal-dialog-custom" style="max-width: 450px;">
<div class="modal-header-custom">
<h5 class="modal-title-custom 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-custom">
<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-custom">
<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>
@@ -0,0 +1,133 @@
// <auto-generated />
using System;
using Cloud;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Cloud.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260817053000_AddUsersAndSyncCardNotes")]
partial class AddUsersAndSyncCardNotes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DatabaseTest.Models.PasskeyCredential", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AaGuid")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("CredentialId")
.IsRequired()
.HasColumnType("bytea");
b.Property<byte[]>("PublicKey")
.IsRequired()
.HasColumnType("bytea");
b.Property<long>("SignCount")
.HasColumnType("bigint");
b.Property<byte[]>("UserHandle")
.IsRequired()
.HasColumnType("bytea");
b.HasKey("Id");
b.ToTable("PasskeyCredentials");
});
modelBuilder.Entity("Model.CardNote", b =>
{
b.Property<string>("Username")
.HasColumnType("text");
b.Property<string>("CardName")
.HasColumnType("text");
b.Property<string>("Note")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Username", "CardName");
b.ToTable("CardNotes");
});
modelBuilder.Entity("Model.User", b =>
{
b.Property<string>("Username")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Username");
b.ToTable("Users");
});
modelBuilder.Entity("Model.UserDeck", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.PrimitiveCollection<string>("Cards")
.IsRequired()
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Divers")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Notes")
.HasColumnType("text");
b.Property<string>("Season")
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("UserDecks");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,75 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cloud.Migrations
{
/// <inheritdoc />
public partial class AddUsersAndSyncCardNotes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_CardNotes",
table: "CardNotes");
migrationBuilder.AddColumn<string>(
name: "Username",
table: "CardNotes",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<DateTime>(
name: "UpdatedAt",
table: "CardNotes",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddPrimaryKey(
name: "PK_CardNotes",
table: "CardNotes",
columns: new[] { "Username", "CardName" });
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Username = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Username);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropPrimaryKey(
name: "PK_CardNotes",
table: "CardNotes");
migrationBuilder.DropColumn(
name: "Username",
table: "CardNotes");
migrationBuilder.DropColumn(
name: "UpdatedAt",
table: "CardNotes");
migrationBuilder.AddPrimaryKey(
name: "PK_CardNotes",
table: "CardNotes",
column: "CardName");
}
}
}
@@ -21,8 +21,11 @@ namespace Cloud.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Chrono.Model.CardNote", b =>
modelBuilder.Entity("Model.CardNote", b =>
{
b.Property<string>("Username")
.HasColumnType("text");
b.Property<string>("CardName")
.HasColumnType("text");
@@ -30,12 +33,32 @@ namespace Cloud.Migrations
.IsRequired()
.HasColumnType("text");
b.HasKey("CardName");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Username", "CardName");
b.ToTable("CardNotes");
});
modelBuilder.Entity("Cloud.Models.PasskeyCredential", b =>
modelBuilder.Entity("Model.User", b =>
{
b.Property<string>("Username")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Username");
b.ToTable("Users");
});
modelBuilder.Entity("DatabaseTest.Models.PasskeyCredential", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -69,7 +92,7 @@ namespace Cloud.Migrations
b.ToTable("PasskeyCredentials");
});
modelBuilder.Entity("Cloud.Models.UserDeck", b =>
modelBuilder.Entity("Model.UserDeck", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
-12
View File
@@ -1,12 +0,0 @@
namespace Cloud.Models;
public class UserDeck
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = "";
public List<string> Cards { get; set; } = [];
public List<string> Divers { get; set; } = [];
public string? Notes { get; set; }
public string? Season { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
+20
View File
@@ -1,4 +1,5 @@
using DatabaseTest.Components;
using DatabaseTest.Models;
using DatabaseTest.Services;
var builder = WebApplication.CreateBuilder(args);
@@ -12,6 +13,25 @@ builder.Services.AddScoped<IDatabaseService, DatabaseService>();
var app = builder.Build();
try
{
using var scope = app.Services.CreateScope();
var configService = scope.ServiceProvider.GetRequiredService<IDatabaseConfigService>();
var dbService = scope.ServiceProvider.GetRequiredService<IDatabaseService>();
if (configService.ProviderType == DatabaseProviderType.PostgreSQL)
{
_ = dbService.ApplyMigrationsAsync();
}
else
{
_ = dbService.EnsureDatabaseCreatedAsync();
}
}
catch
{
// Ignore if database is temporarily offline on startup
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
@@ -404,6 +404,9 @@ public class DatabaseService : IDatabaseService
public async Task<OperationResult> SaveUserAsync(User user, bool isNew)
{
user.Username = user.Username?.Trim() ?? "";
user.Password = user.Password?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(user.Username))
return OperationResult.Fail("Username is required.");
if (string.IsNullOrWhiteSpace(user.Password))
@@ -487,11 +490,13 @@ public class DatabaseService : IDatabaseService
public async Task<OperationResult> SaveCardNoteAsync(CardNote note, bool isNew)
{
note.CardName = note.CardName?.Trim() ?? "";
note.Username = note.Username?.Trim() ?? "";
note.Note = note.Note?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(note.CardName))
return OperationResult.Fail("Card name is required.");
note.Username ??= "";
try
{
await using var db = _configService.CreateDbContext();