...
This commit is contained in:
@@ -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>
|
||||
|
||||
+133
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user