diff --git a/Hcs.WebApp/Components/Layout/MainLayout.razor b/Hcs.WebApp/Components/Layout/MainLayout.razor
index ecdfaa9..aae7479 100644
--- a/Hcs.WebApp/Components/Layout/MainLayout.razor
+++ b/Hcs.WebApp/Components/Layout/MainLayout.razor
@@ -1,5 +1,7 @@
@inherits LayoutComponentBase
+@implements IDisposable
+
@inject NavigationManager NavigationManager
@@ -12,12 +14,30 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@Body
@@ -32,4 +52,24 @@
@code {
bool sidebarExpanded = true;
+ string? currentUrl;
+
+ protected override void OnInitialized()
+ {
+ currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
+
+ NavigationManager.LocationChanged += OnLocationChanged;
+ }
+
+ public void Dispose()
+ {
+ NavigationManager.LocationChanged -= OnLocationChanged;
+ }
+
+ private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
+ {
+ currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
+
+ StateHasChanged();
+ }
}
diff --git a/Hcs.WebApp/Components/Pages/Test/Export.razor b/Hcs.WebApp/Components/Pages/Test/Export.razor
index 88efafa..4368bc1 100644
--- a/Hcs.WebApp/Components/Pages/Test/Export.razor
+++ b/Hcs.WebApp/Components/Pages/Test/Export.razor
@@ -1,5 +1,6 @@
@page "/test/export"
+@using Microsoft.AspNetCore.Authorization
@using Hcs.Broker
@using Hcs.Broker.Logger
@using Hcs.Broker.MessageCapturer
@@ -8,6 +9,10 @@
@using Hcs.WebApp.Config
@using Hcs.WebApp.Utils
+@implements IDisposable
+
+@attribute [Authorize]
+
@inject NavigationManager NavigationManager
@inject IConfiguration Configuration
@@ -82,6 +87,14 @@
client.SetSigningCertificate(brokerConfig.CertificateSerialNumber);
}
+ public void Dispose()
+ {
+ if (messageCapturer != null)
+ {
+ messageCapturer.OnFileWritten -= OnFileWritten;
+ }
+ }
+
void OnLog(string log)
{
console.Log(log);
diff --git a/Hcs.WebApp/Components/_Imports.razor b/Hcs.WebApp/Components/_Imports.razor
index b276bdf..1d7cf24 100644
--- a/Hcs.WebApp/Components/_Imports.razor
+++ b/Hcs.WebApp/Components/_Imports.razor
@@ -1,12 +1,14 @@
@using System.Net.Http
@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
-@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
-@using Hcs.WebApp
-@using Hcs.WebApp.Components
@using Radzen
@using Radzen.Blazor
+@using Hcs.WebApp
+@using Hcs.WebApp.Components
+
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
diff --git a/Hcs.WebApp/Data/AppIdentityDbContext.cs b/Hcs.WebApp/Data/AppIdentityDbContext.cs
new file mode 100644
index 0000000..3e760cd
--- /dev/null
+++ b/Hcs.WebApp/Data/AppIdentityDbContext.cs
@@ -0,0 +1,7 @@
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+
+namespace Hcs.WebApp.Data
+{
+ internal class AppIdentityDbContext(DbContextOptions options) : IdentityDbContext(options) { }
+}
diff --git a/Hcs.WebApp/Data/AppUser.cs b/Hcs.WebApp/Data/AppUser.cs
new file mode 100644
index 0000000..21b6d93
--- /dev/null
+++ b/Hcs.WebApp/Data/AppUser.cs
@@ -0,0 +1,6 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace Hcs.WebApp.Data
+{
+ internal class AppUser : IdentityUser { }
+}
diff --git a/Hcs.WebApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/Hcs.WebApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
new file mode 100644
index 0000000..8bfd051
--- /dev/null
+++ b/Hcs.WebApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
@@ -0,0 +1,274 @@
+//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+namespace Hcs.WebApp.Data.Migrations
+{
+ [DbContext(typeof(AppIdentityDbContext))]
+ [Migration("00000000000000_CreateIdentitySchema")]
+ partial class CreateIdentitySchema
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Hcs.WebApp.Data.AppUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Hcs.WebApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/Hcs.WebApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs
new file mode 100644
index 0000000..8cb9094
--- /dev/null
+++ b/Hcs.WebApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs
@@ -0,0 +1,222 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+namespace Hcs.WebApp.Data.Migrations
+{
+ ///
+ internal partial class CreateIdentitySchema : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AspNetRoles",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(450)", nullable: false),
+ Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoles", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUsers",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(450)", nullable: false),
+ UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ EmailConfirmed = table.Column(type: "bit", nullable: false),
+ PasswordHash = table.Column(type: "nvarchar(max)", nullable: true),
+ SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true),
+ ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true),
+ PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true),
+ PhoneNumberConfirmed = table.Column(type: "bit", nullable: false),
+ TwoFactorEnabled = table.Column(type: "bit", nullable: false),
+ LockoutEnd = table.Column(type: "datetimeoffset", nullable: true),
+ LockoutEnabled = table.Column(type: "bit", nullable: false),
+ AccessFailedCount = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUsers", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetRoleClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ RoleId = table.Column(type: "nvarchar(450)", nullable: false),
+ ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
+ ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
+ ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetUserClaims_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserLogins",
+ columns: table => new
+ {
+ LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
+ ProviderKey = table.Column(type: "nvarchar(450)", nullable: false),
+ ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true),
+ UserId = table.Column(type: "nvarchar(450)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
+ table.ForeignKey(
+ name: "FK_AspNetUserLogins_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserRoles",
+ columns: table => new
+ {
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ RoleId = table.Column(type: "nvarchar(450)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserTokens",
+ columns: table => new
+ {
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
+ Name = table.Column(type: "nvarchar(450)", nullable: false),
+ Value = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
+ table.ForeignKey(
+ name: "FK_AspNetUserTokens_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetRoleClaims_RoleId",
+ table: "AspNetRoleClaims",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "RoleNameIndex",
+ table: "AspNetRoles",
+ column: "NormalizedName",
+ unique: true,
+ filter: "[NormalizedName] IS NOT NULL");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserClaims_UserId",
+ table: "AspNetUserClaims",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserLogins_UserId",
+ table: "AspNetUserLogins",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserRoles_RoleId",
+ table: "AspNetUserRoles",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "EmailIndex",
+ table: "AspNetUsers",
+ column: "NormalizedEmail");
+
+ migrationBuilder.CreateIndex(
+ name: "UserNameIndex",
+ table: "AspNetUsers",
+ column: "NormalizedUserName",
+ unique: true,
+ filter: "[NormalizedUserName] IS NOT NULL");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AspNetRoleClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserLogins");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserTokens");
+
+ migrationBuilder.DropTable(
+ name: "AspNetRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUsers");
+ }
+ }
+}
diff --git a/Hcs.WebApp/Data/Migrations/AppIdentityDbContextModelSnapshot.cs b/Hcs.WebApp/Data/Migrations/AppIdentityDbContextModelSnapshot.cs
new file mode 100644
index 0000000..2fa491b
--- /dev/null
+++ b/Hcs.WebApp/Data/Migrations/AppIdentityDbContextModelSnapshot.cs
@@ -0,0 +1,269 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Hcs.WebApp.Data.Migrations
+{
+ [DbContext(typeof(AppIdentityDbContext))]
+ internal class AppIdentityDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Hcs.WebApp.Data.AppUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Hcs.WebApp.Data.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Hcs.WebApp/Hcs.WebApp.csproj b/Hcs.WebApp/Hcs.WebApp.csproj
index 1315484..65cd85e 100644
--- a/Hcs.WebApp/Hcs.WebApp.csproj
+++ b/Hcs.WebApp/Hcs.WebApp.csproj
@@ -7,6 +7,15 @@
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/Hcs.WebApp/Identity/IdentityComponentsEndpointRouteBuilderExtensions.cs b/Hcs.WebApp/Identity/IdentityComponentsEndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000..58cc656
--- /dev/null
+++ b/Hcs.WebApp/Identity/IdentityComponentsEndpointRouteBuilderExtensions.cs
@@ -0,0 +1,27 @@
+using Hcs.WebApp.Data;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using System.Security.Claims;
+
+namespace Hcs.WebApp.Identity
+{
+ internal static class IdentityComponentsEndpointRouteBuilderExtensions
+ {
+ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+
+ var accountGroup = endpoints.MapGroup("/account");
+ accountGroup.MapPost("/logout", async (
+ ClaimsPrincipal user,
+ SignInManager signInManager,
+ [FromForm] string returnUrl) =>
+ {
+ await signInManager.SignOutAsync();
+ return TypedResults.LocalRedirect($"~/{returnUrl}");
+ });
+
+ return accountGroup;
+ }
+ }
+}
diff --git a/Hcs.WebApp/Identity/IdentityRedirectManager.cs b/Hcs.WebApp/Identity/IdentityRedirectManager.cs
new file mode 100644
index 0000000..ef2bb53
--- /dev/null
+++ b/Hcs.WebApp/Identity/IdentityRedirectManager.cs
@@ -0,0 +1,62 @@
+using Microsoft.AspNetCore.Components;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Hcs.WebApp.Identity
+{
+ internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
+ {
+ public const string STATUS_COOKIE_NAME = "Identity.StatusMessage";
+
+ private static readonly CookieBuilder statusCookieBuilder = new()
+ {
+ SameSite = SameSiteMode.Strict,
+ HttpOnly = true,
+ IsEssential = true,
+ MaxAge = TimeSpan.FromSeconds(5),
+ };
+
+ private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
+
+ [DoesNotReturn]
+ public void RedirectTo(string? uri)
+ {
+ uri ??= "";
+
+ if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
+ {
+ uri = navigationManager.ToBaseRelativePath(uri);
+ }
+
+ navigationManager.NavigateTo(uri);
+
+ throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} может быть использован только при статичном рендеринге");
+ }
+
+ [DoesNotReturn]
+ public void RedirectTo(string uri, Dictionary queryParameters)
+ {
+ var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
+ var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
+ RedirectTo(newUri);
+ }
+
+ [DoesNotReturn]
+ public void RedirectToWithStatus(string uri, string message, HttpContext context)
+ {
+ context.Response.Cookies.Append(STATUS_COOKIE_NAME, message, statusCookieBuilder.Build(context));
+ RedirectTo(uri);
+ }
+
+ [DoesNotReturn]
+ public void RedirectToCurrentPage()
+ {
+ RedirectTo(CurrentPath);
+ }
+
+ [DoesNotReturn]
+ public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
+ {
+ RedirectToWithStatus(CurrentPath, message, context);
+ }
+ }
+}
diff --git a/Hcs.WebApp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs b/Hcs.WebApp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs
new file mode 100644
index 0000000..dd98fe2
--- /dev/null
+++ b/Hcs.WebApp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs
@@ -0,0 +1,45 @@
+using Hcs.WebApp.Data;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Components.Server;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using System.Security.Claims;
+
+namespace Hcs.WebApp.Identity
+{
+ internal sealed class IdentityRevalidatingAuthenticationStateProvider(
+ ILoggerFactory loggerFactory,
+ IServiceScopeFactory scopeFactory,
+ IOptions options) : RevalidatingServerAuthenticationStateProvider(loggerFactory)
+ {
+ protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
+
+ protected override async Task ValidateAuthenticationStateAsync(
+ AuthenticationState authenticationState, CancellationToken cancellationToken)
+ {
+ await using var scope = scopeFactory.CreateAsyncScope();
+
+ var userManager = scope.ServiceProvider.GetRequiredService>();
+ return await ValidateSecurityStampAsync(userManager, authenticationState.User);
+ }
+
+ private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal)
+ {
+ var user = await userManager.GetUserAsync(principal);
+ if (user is null)
+ {
+ return false;
+ }
+ else if (!userManager.SupportsUserSecurityStamp)
+ {
+ return true;
+ }
+ else
+ {
+ var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
+ var userStamp = await userManager.GetSecurityStampAsync(user);
+ return principalStamp == userStamp;
+ }
+ }
+ }
+}
diff --git a/Hcs.WebApp/Identity/IdentityUserAccessor.cs b/Hcs.WebApp/Identity/IdentityUserAccessor.cs
new file mode 100644
index 0000000..0446880
--- /dev/null
+++ b/Hcs.WebApp/Identity/IdentityUserAccessor.cs
@@ -0,0 +1,18 @@
+using Hcs.WebApp.Data;
+using Microsoft.AspNetCore.Identity;
+
+namespace Hcs.WebApp.Identity
+{
+ internal sealed class IdentityUserAccessor(UserManager userManager, IdentityRedirectManager redirectManager)
+ {
+ public async Task GetRequiredUserAsync(HttpContext context)
+ {
+ var user = await userManager.GetUserAsync(context.User);
+ if (user is null)
+ {
+ redirectManager.RedirectToWithStatus("account/invalid_user", $"Ошибка: Не удалось загрузить пользователя с идентификатором '{userManager.GetUserId(context.User)}'", context);
+ }
+ return user;
+ }
+ }
+}
diff --git a/Hcs.WebApp/Program.cs b/Hcs.WebApp/Program.cs
index 7e956cc..ee6889a 100644
--- a/Hcs.WebApp/Program.cs
+++ b/Hcs.WebApp/Program.cs
@@ -1,15 +1,49 @@
using Hcs.WebApp.Components;
+using Hcs.WebApp.Data;
+using Hcs.WebApp.Identity;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
using Radzen;
var builder = WebApplication.CreateBuilder(args);
+
builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddRadzenComponents();
+builder.Services.AddCascadingAuthenticationState();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+builder.Services
+ .AddAuthentication(options =>
+ {
+ options.DefaultScheme = IdentityConstants.ApplicationScheme;
+ options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
+ })
+ .AddIdentityCookies();
+
+var connectionString = builder.Configuration.GetConnectionString("IdentityConnection") ?? throw new InvalidOperationException(" 'IdentityConnection'");
+builder.Services.AddDbContext(options => options.UseSqlServer(connectionString));
+builder.Services.AddDatabaseDeveloperPageExceptionFilter();
+
+builder.Services
+ .AddIdentityCore()
+ .AddEntityFrameworkStores()
+ .AddSignInManager()
+ .AddDefaultTokenProviders();
+
var app = builder.Build();
-if (!app.Environment.IsDevelopment())
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseMigrationsEndPoint();
+}
+else
{
app.UseExceptionHandler("/error", createScopeForErrors: true);
app.UseHsts();
@@ -24,4 +58,6 @@ app
.MapRazorComponents()
.AddInteractiveServerRenderMode();
+app.MapAdditionalIdentityEndpoints();
+
app.Run();
diff --git a/Hcs.WebApp/appsettings.Development.json b/Hcs.WebApp/appsettings.Development.json
index 35f62ad..1f13e91 100644
--- a/Hcs.WebApp/appsettings.Development.json
+++ b/Hcs.WebApp/appsettings.Development.json
@@ -11,5 +11,8 @@
"OrgPPAGUID": "ccd7fa02-a2bf-428a-984b-faef69ae0eb2",
"ExecutorGUID": "ccd7fa02-a2bf-428a-984b-faef69ae0eb2",
"CertificateSerialNumber": "0636D2330032B3C38A4A26D765C787C248"
+ },
+ "ConnectionStrings": {
+ "AuthConnection": "Server=localhost\\SQLEXPRESS;Database=hcs_web_app_identity;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=Yes"
}
}
diff --git a/Hcs.WebApp/appsettings.json b/Hcs.WebApp/appsettings.json
index 927e57b..ff571cc 100644
--- a/Hcs.WebApp/appsettings.json
+++ b/Hcs.WebApp/appsettings.json
@@ -11,5 +11,8 @@
"OrgPPAGUID": "ccd7fa02-a2bf-428a-984b-faef69ae0eb2",
"ExecutorGUID": "ccd7fa02-a2bf-428a-984b-faef69ae0eb2",
"CertificateSerialNumber": "0636D2330032B3C38A4A26D765C787C248"
+ },
+ "ConnectionStrings": {
+ "IdentityConnection": "Server=localhost\\SQLEXPRESS;Database=hcs_web_app_identity;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=Yes"
}
}