mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-05-10 08:34:11 +02:00
Replace ASP.NET Identity with a custom solution
This commit is contained in:
parent
956f1e779b
commit
346eab0b1b
Packages.props
Server
Phantom.Server.Database.Postgres/Migrations
20231008122637_ReplaceIdentity.Designer.cs20231008122637_ReplaceIdentity.cs20231008123315_ReplaceIdentity2.Designer.cs20231008123315_ReplaceIdentity2.csApplicationDbContextModelSnapshot.cs
Phantom.Server.Database
ApplicationDbContext.cs
Entities
AuditLogEntity.csRoleEntity.csRolePermissionEntity.csUserEntity.csUserPermissionEntity.csUserRoleEntity.cs
Phantom.Server.Database.csprojPhantom.Server.Services
Audit
Users
Phantom.Server.Web.Identity
Authentication
Authorization
Data
Interfaces
Phantom.Server.Web.Identity.csprojPhantomIdentityConfigurator.csPhantomIdentityExtensions.csPhantomIdentityMiddleware.csPhantom.Server.Web
Phantom.Server
Utils/Phantom.Utils
@ -3,7 +3,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.AspNetCore.Components.Authorization" Version="7.0.11" />
|
||||
<PackageReference Update="Microsoft.AspNetCore.Components.Web" Version="7.0.11" />
|
||||
<PackageReference Update="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.11" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" />
|
||||
<PackageReference Update="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
|
||||
</ItemGroup>
|
||||
|
180
Server/Phantom.Server.Database.Postgres/Migrations/20231008122637_ReplaceIdentity.Designer.cs
generated
Normal file
180
Server/Phantom.Server.Database.Postgres/Migrations/20231008122637_ReplaceIdentity.Designer.cs
generated
Normal file
@ -0,0 +1,180 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Phantom.Server.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Phantom.Server.Database.Postgres.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20231008122637_ReplaceIdentity")]
|
||||
partial class ReplaceIdentity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BuildVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("MaxInstances")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<ushort>("MaxMemory")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ProtocolVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("AgentGuid");
|
||||
|
||||
b.ToTable("Agents", "agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<JsonDocument>("Data")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UtcTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AuditLog", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.EventLogEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("EventGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("Data")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UtcTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("EventGuid");
|
||||
|
||||
b.ToTable("EventLog", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("InstanceGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("InstanceName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("JavaRuntimeGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("JvmArguments")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("LaunchAutomatically")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<ushort>("MemoryAllocation")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MinecraftServerKind")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MinecraftVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("RconPort")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ServerPort")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("InstanceGuid");
|
||||
|
||||
b.ToTable("Instances", "agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.PermissionEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Permissions", "identity");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,373 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Phantom.Server.Database.Postgres.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReplaceIdentity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AuditLog_Users_UserId",
|
||||
schema: "system",
|
||||
table: "AuditLog");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RoleClaims",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RolePermissions",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserClaims",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserLogins",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserPermissions",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRoles",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserTokens",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Roles",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AuditLog_UserId",
|
||||
schema: "system",
|
||||
table: "AuditLog");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
schema: "system",
|
||||
table: "AuditLog");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "UserGuid",
|
||||
schema: "system",
|
||||
table: "AuditLog",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserGuid",
|
||||
schema: "system",
|
||||
table: "AuditLog");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UserId",
|
||||
schema: "system",
|
||||
table: "AuditLog",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Roles",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Roles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RoleClaims",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true),
|
||||
RoleId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RoleClaims_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RolePermissions",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
RoleId = table.Column<string>(type: "text", nullable: false),
|
||||
PermissionId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RolePermissions", x => new { x.RoleId, x.PermissionId });
|
||||
table.ForeignKey(
|
||||
name: "FK_RolePermissions_Permissions_PermissionId",
|
||||
column: x => x.PermissionId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Permissions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_RolePermissions_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserClaims",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true),
|
||||
UserId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserClaims_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserLogins",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
|
||||
UserId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserLogins_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserPermissions",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
PermissionId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserPermissions", x => new { x.UserId, x.PermissionId });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserPermissions_Permissions_PermissionId",
|
||||
column: x => x.PermissionId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Permissions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserPermissions_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRoles",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
RoleId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserTokens",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Value = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserTokens_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLog_UserId",
|
||||
schema: "system",
|
||||
table: "AuditLog",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RoleClaims_RoleId",
|
||||
schema: "identity",
|
||||
table: "RoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RolePermissions_PermissionId",
|
||||
schema: "identity",
|
||||
table: "RolePermissions",
|
||||
column: "PermissionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
schema: "identity",
|
||||
table: "Roles",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserClaims_UserId",
|
||||
schema: "identity",
|
||||
table: "UserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserLogins_UserId",
|
||||
schema: "identity",
|
||||
table: "UserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserPermissions_PermissionId",
|
||||
schema: "identity",
|
||||
table: "UserPermissions",
|
||||
column: "PermissionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoles_RoleId",
|
||||
schema: "identity",
|
||||
table: "UserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AuditLog_Users_UserId",
|
||||
schema: "system",
|
||||
table: "AuditLog",
|
||||
column: "UserId",
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
323
Server/Phantom.Server.Database.Postgres/Migrations/20231008123315_ReplaceIdentity2.Designer.cs
generated
Normal file
323
Server/Phantom.Server.Database.Postgres/Migrations/20231008123315_ReplaceIdentity2.Designer.cs
generated
Normal file
@ -0,0 +1,323 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Phantom.Server.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Phantom.Server.Database.Postgres.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20231008123315_ReplaceIdentity2")]
|
||||
partial class ReplaceIdentity2
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BuildVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("MaxInstances")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<ushort>("MaxMemory")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ProtocolVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("AgentGuid");
|
||||
|
||||
b.ToTable("Agents", "agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<JsonDocument>("Data")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UtcTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserGuid");
|
||||
|
||||
b.ToTable("AuditLog", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.EventLogEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("EventGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("Data")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UtcTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("EventGuid");
|
||||
|
||||
b.ToTable("EventLog", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("InstanceGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("InstanceName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("JavaRuntimeGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("JvmArguments")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("LaunchAutomatically")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<ushort>("MemoryAllocation")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MinecraftServerKind")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MinecraftVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("RconPort")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ServerPort")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("InstanceGuid");
|
||||
|
||||
b.ToTable("Instances", "agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.PermissionEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Permissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.RoleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("RoleGuid");
|
||||
|
||||
b.ToTable("Roles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.RolePermissionEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PermissionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("RoleGuid", "PermissionId");
|
||||
|
||||
b.HasIndex("PermissionId");
|
||||
|
||||
b.ToTable("RolePermissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserGuid");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserPermissionEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PermissionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserGuid", "PermissionId");
|
||||
|
||||
b.HasIndex("PermissionId");
|
||||
|
||||
b.ToTable("UserPermissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserRoleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserGuid", "RoleGuid");
|
||||
|
||||
b.HasIndex("RoleGuid");
|
||||
|
||||
b.ToTable("UserRoles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Server.Database.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.RolePermissionEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Server.Database.Entities.PermissionEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Server.Database.Entities.RoleEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserPermissionEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Server.Database.Entities.PermissionEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Server.Database.Entities.UserEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserRoleEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Server.Database.Entities.RoleEntity", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Server.Database.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Phantom.Server.Database.Postgres.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReplaceIdentity2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Roles",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
RoleGuid = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Roles", x => x.RoleGuid);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserGuid = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.UserGuid);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RolePermissions",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
RoleGuid = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PermissionId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RolePermissions", x => new { x.RoleGuid, x.PermissionId });
|
||||
table.ForeignKey(
|
||||
name: "FK_RolePermissions_Permissions_PermissionId",
|
||||
column: x => x.PermissionId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Permissions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_RolePermissions_Roles_RoleGuid",
|
||||
column: x => x.RoleGuid,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "RoleGuid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserPermissions",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserGuid = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PermissionId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserPermissions", x => new { x.UserGuid, x.PermissionId });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserPermissions_Permissions_PermissionId",
|
||||
column: x => x.PermissionId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Permissions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserPermissions_Users_UserGuid",
|
||||
column: x => x.UserGuid,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserGuid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRoles",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserGuid = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
RoleGuid = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserRoles", x => new { x.UserGuid, x.RoleGuid });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Roles_RoleGuid",
|
||||
column: x => x.RoleGuid,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "RoleGuid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Users_UserGuid",
|
||||
column: x => x.UserGuid,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserGuid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLog_UserGuid",
|
||||
schema: "system",
|
||||
table: "AuditLog",
|
||||
column: "UserGuid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RolePermissions_PermissionId",
|
||||
schema: "identity",
|
||||
table: "RolePermissions",
|
||||
column: "PermissionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserPermissions_PermissionId",
|
||||
schema: "identity",
|
||||
table: "UserPermissions",
|
||||
column: "PermissionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoles_RoleGuid",
|
||||
schema: "identity",
|
||||
table: "UserRoles",
|
||||
column: "RoleGuid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_Name",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AuditLog_Users_UserGuid",
|
||||
schema: "system",
|
||||
table: "AuditLog",
|
||||
column: "UserGuid",
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserGuid",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AuditLog_Users_UserGuid",
|
||||
schema: "system",
|
||||
table: "AuditLog");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RolePermissions",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserPermissions",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRoles",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Roles",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AuditLog_UserGuid",
|
||||
schema: "system",
|
||||
table: "AuditLog");
|
||||
}
|
||||
}
|
||||
}
|
@ -18,207 +18,11 @@ namespace Phantom.Server.Database.Postgres.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.1")
|
||||
.HasAnnotation("ProductVersion", "7.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("Roles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("RoleClaims", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserClaims", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserLogins", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("UserRoles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("AgentGuid")
|
||||
@ -270,15 +74,15 @@ namespace Phantom.Server.Database.Postgres.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
b.Property<Guid?>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UtcTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
b.HasIndex("UserGuid");
|
||||
|
||||
b.ToTable("AuditLog", "system");
|
||||
});
|
||||
@ -370,92 +174,94 @@ namespace Phantom.Server.Database.Postgres.Migrations
|
||||
b.ToTable("Permissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.RoleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("RoleGuid");
|
||||
|
||||
b.ToTable("Roles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.RolePermissionEntity", b =>
|
||||
{
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PermissionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("RoleId", "PermissionId");
|
||||
b.HasKey("RoleGuid", "PermissionId");
|
||||
|
||||
b.HasIndex("PermissionId");
|
||||
|
||||
b.ToTable("RolePermissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserGuid");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserPermissionEntity", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PermissionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "PermissionId");
|
||||
b.HasKey("UserGuid", "PermissionId");
|
||||
|
||||
b.HasIndex("PermissionId");
|
||||
|
||||
b.ToTable("UserPermissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserRoleEntity", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
b.HasKey("UserGuid", "RoleGuid");
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
b.HasIndex("RoleGuid");
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
b.ToTable("UserRoles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
|
||||
b.HasOne("Phantom.Server.Database.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId");
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
@ -468,9 +274,9 @@ namespace Phantom.Server.Database.Postgres.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
b.HasOne("Phantom.Server.Database.Entities.RoleEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.HasForeignKey("RoleGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
@ -483,12 +289,31 @@ namespace Phantom.Server.Database.Postgres.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
b.HasOne("Phantom.Server.Database.Entities.UserEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Server.Database.Entities.UserRoleEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Server.Database.Entities.RoleEntity", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Server.Database.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Phantom.Common.Data;
|
||||
@ -13,8 +11,12 @@ using Phantom.Server.Database.Factories;
|
||||
namespace Phantom.Server.Database;
|
||||
|
||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
||||
public class ApplicationDbContext : IdentityDbContext {
|
||||
public class ApplicationDbContext : DbContext {
|
||||
public DbSet<UserEntity> Users { get; set; } = null!;
|
||||
public DbSet<RoleEntity> Roles { get; set; } = null!;
|
||||
public DbSet<PermissionEntity> Permissions { get; set; } = null!;
|
||||
|
||||
public DbSet<UserRoleEntity> UserRoles { get; set; } = null!;
|
||||
public DbSet<UserPermissionEntity> UserPermissions { get; set; } = null!;
|
||||
public DbSet<RolePermissionEntity> RolePermissions { get; set; } = null!;
|
||||
|
||||
@ -34,26 +36,29 @@ public class ApplicationDbContext : IdentityDbContext {
|
||||
protected override void OnModelCreating(ModelBuilder builder) {
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
const string IdentitySchema = "identity";
|
||||
builder.Entity<AuditLogEntity>(static b => {
|
||||
b.HasOne(static e => e.User).WithMany().HasForeignKey(static e => e.UserGuid).IsRequired(false).OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
builder.Entity<IdentityRole>().ToTable("Roles", schema: IdentitySchema);
|
||||
builder.Entity<IdentityRoleClaim<string>>().ToTable("RoleClaims", schema: IdentitySchema);
|
||||
builder.Entity<UserEntity>(static b => {
|
||||
b.HasIndex(static e => e.Name).IsUnique();
|
||||
});
|
||||
|
||||
builder.Entity<IdentityUser>().ToTable("Users", schema: IdentitySchema);
|
||||
builder.Entity<IdentityUserRole<string>>().ToTable("UserRoles", schema: IdentitySchema);
|
||||
builder.Entity<IdentityUserLogin<string>>().ToTable("UserLogins", schema: IdentitySchema);
|
||||
builder.Entity<IdentityUserToken<string>>().ToTable("UserTokens", schema: IdentitySchema);
|
||||
builder.Entity<IdentityUserClaim<string>>().ToTable("UserClaims", schema: IdentitySchema);
|
||||
builder.Entity<UserRoleEntity>(static b => {
|
||||
b.HasKey(static e => new { UserId = e.UserGuid, RoleId = e.RoleGuid });
|
||||
b.HasOne(static e => e.User).WithMany().HasForeignKey(static e => e.UserGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(static e => e.Role).WithMany().HasForeignKey(static e => e.RoleGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<UserPermissionEntity>(static b => {
|
||||
b.HasKey(static e => new { e.UserId, e.PermissionId });
|
||||
b.HasOne<IdentityUser>().WithMany().HasForeignKey(static e => e.UserId).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasKey(static e => new { UserId = e.UserGuid, e.PermissionId });
|
||||
b.HasOne<UserEntity>().WithMany().HasForeignKey(static e => e.UserGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne<PermissionEntity>().WithMany().HasForeignKey(static e => e.PermissionId).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<RolePermissionEntity>(static b => {
|
||||
b.HasKey(static e => new { e.RoleId, e.PermissionId });
|
||||
b.HasOne<IdentityRole>().WithMany().HasForeignKey(static e => e.RoleId).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasKey(static e => new { RoleId = e.RoleGuid, e.PermissionId });
|
||||
b.HasOne<RoleEntity>().WithMany().HasForeignKey(static e => e.RoleGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne<PermissionEntity>().WithMany().HasForeignKey(static e => e.PermissionId).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Server.Database.Enums;
|
||||
|
||||
namespace Phantom.Server.Database.Entities;
|
||||
@ -16,22 +15,22 @@ public class AuditLogEntity : IDisposable {
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
public long Id { get; set; }
|
||||
|
||||
public string? UserId { get; set; }
|
||||
public Guid? UserGuid { get; set; }
|
||||
public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
||||
public AuditLogEventType EventType { get; set; }
|
||||
public AuditLogSubjectType SubjectType { get; set; }
|
||||
public string SubjectId { get; set; }
|
||||
public JsonDocument? Data { get; set; }
|
||||
|
||||
public virtual IdentityUser? User { get; set; }
|
||||
public virtual UserEntity? User { get; set; }
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
internal AuditLogEntity() {
|
||||
SubjectId = string.Empty;
|
||||
}
|
||||
|
||||
public AuditLogEntity(string? userId, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? data) {
|
||||
UserId = userId;
|
||||
public AuditLogEntity(Guid? userGuid, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? data) {
|
||||
UserGuid = userGuid;
|
||||
UtcTime = DateTime.UtcNow;
|
||||
EventType = eventType;
|
||||
SubjectType = eventType.GetSubjectType();
|
||||
|
17
Server/Phantom.Server.Database/Entities/RoleEntity.cs
Normal file
17
Server/Phantom.Server.Database/Entities/RoleEntity.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Phantom.Server.Database.Entities;
|
||||
|
||||
[Table("Roles", Schema = "identity")]
|
||||
public sealed class RoleEntity {
|
||||
[Key]
|
||||
public Guid RoleGuid { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public RoleEntity(Guid roleGuid, string name) {
|
||||
RoleGuid = roleGuid;
|
||||
Name = name;
|
||||
}
|
||||
}
|
@ -4,11 +4,11 @@ namespace Phantom.Server.Database.Entities;
|
||||
|
||||
[Table("RolePermissions", Schema = "identity")]
|
||||
public sealed class RolePermissionEntity {
|
||||
public string RoleId { get; set; }
|
||||
public Guid RoleGuid { get; set; }
|
||||
public string PermissionId { get; set; }
|
||||
|
||||
public RolePermissionEntity(string roleId, string permissionId) {
|
||||
RoleId = roleId;
|
||||
public RolePermissionEntity(Guid roleGuid, string permissionId) {
|
||||
RoleGuid = roleGuid;
|
||||
PermissionId = permissionId;
|
||||
}
|
||||
}
|
||||
|
19
Server/Phantom.Server.Database/Entities/UserEntity.cs
Normal file
19
Server/Phantom.Server.Database/Entities/UserEntity.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Phantom.Server.Database.Entities;
|
||||
|
||||
[Table("Users", Schema = "identity")]
|
||||
public sealed class UserEntity {
|
||||
[Key]
|
||||
public Guid UserGuid { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string PasswordHash { get; set; }
|
||||
|
||||
public UserEntity(Guid userGuid, string name) {
|
||||
UserGuid = userGuid;
|
||||
Name = name;
|
||||
PasswordHash = null!;
|
||||
}
|
||||
}
|
@ -4,11 +4,11 @@ namespace Phantom.Server.Database.Entities;
|
||||
|
||||
[Table("UserPermissions", Schema = "identity")]
|
||||
public sealed class UserPermissionEntity {
|
||||
public string UserId { get; set; }
|
||||
public Guid UserGuid { get; set; }
|
||||
public string PermissionId { get; set; }
|
||||
|
||||
public UserPermissionEntity(string userId, string permissionId) {
|
||||
UserId = userId;
|
||||
public UserPermissionEntity(Guid userGuid, string permissionId) {
|
||||
UserGuid = userGuid;
|
||||
PermissionId = permissionId;
|
||||
}
|
||||
}
|
||||
|
19
Server/Phantom.Server.Database/Entities/UserRoleEntity.cs
Normal file
19
Server/Phantom.Server.Database/Entities/UserRoleEntity.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Phantom.Server.Database.Entities;
|
||||
|
||||
[Table("UserRoles", Schema = "identity")]
|
||||
public sealed class UserRoleEntity {
|
||||
public Guid UserGuid { get; set; }
|
||||
public Guid RoleGuid { get; set; }
|
||||
|
||||
public UserEntity User { get; set; }
|
||||
public RoleEntity Role { get; set; }
|
||||
|
||||
public UserRoleEntity(Guid userGuid, Guid roleGuid) {
|
||||
UserGuid = userGuid;
|
||||
RoleGuid = roleGuid;
|
||||
User = null!;
|
||||
Role = null!;
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -1,30 +1,30 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Server.Database.Entities;
|
||||
using Phantom.Server.Database.Enums;
|
||||
|
||||
namespace Phantom.Server.Services.Audit;
|
||||
|
||||
public sealed partial class AuditLog {
|
||||
public Task AddAdministratorUserCreatedEvent(IdentityUser administratorUser) {
|
||||
return AddItem(AuditLogEventType.AdministratorUserCreated, administratorUser.Id);
|
||||
public Task AddAdministratorUserCreatedEvent(UserEntity administratorUser) {
|
||||
return AddItem(AuditLogEventType.AdministratorUserCreated, administratorUser.UserGuid.ToString());
|
||||
}
|
||||
|
||||
public Task AddAdministratorUserModifiedEvent(IdentityUser administratorUser) {
|
||||
return AddItem(AuditLogEventType.AdministratorUserModified, administratorUser.Id);
|
||||
public Task AddAdministratorUserModifiedEvent(UserEntity administratorUser) {
|
||||
return AddItem(AuditLogEventType.AdministratorUserModified, administratorUser.UserGuid.ToString());
|
||||
}
|
||||
|
||||
public void AddUserLoggedInEvent(string userId) {
|
||||
AddItem(userId, AuditLogEventType.UserLoggedIn, userId);
|
||||
public void AddUserLoggedInEvent(UserEntity user) {
|
||||
AddItem(user.UserGuid, AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
|
||||
}
|
||||
|
||||
public void AddUserLoggedOutEvent(string userId) {
|
||||
AddItem(userId, AuditLogEventType.UserLoggedOut, userId);
|
||||
public void AddUserLoggedOutEvent(Guid userGuid) {
|
||||
AddItem(userGuid, AuditLogEventType.UserLoggedOut, userGuid.ToString());
|
||||
}
|
||||
|
||||
public Task AddUserCreatedEvent(IdentityUser user) {
|
||||
return AddItem(AuditLogEventType.UserCreated, user.Id);
|
||||
public Task AddUserCreatedEvent(UserEntity user) {
|
||||
return AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString());
|
||||
}
|
||||
|
||||
public Task AddUserRolesChangedEvent(IdentityUser user, List<string> addedToRoles, List<string> removedFromRoles) {
|
||||
public Task AddUserRolesChangedEvent(UserEntity user, List<string> addedToRoles, List<string> removedFromRoles) {
|
||||
var extra = new Dictionary<string, object?>();
|
||||
|
||||
if (addedToRoles.Count > 0) {
|
||||
@ -35,12 +35,12 @@ public sealed partial class AuditLog {
|
||||
extra["removedFromRoles"] = removedFromRoles;
|
||||
}
|
||||
|
||||
return AddItem(AuditLogEventType.UserRolesChanged, user.Id, extra);
|
||||
return AddItem(AuditLogEventType.UserRolesChanged, user.UserGuid.ToString(), extra);
|
||||
}
|
||||
|
||||
public Task AddUserDeletedEvent(IdentityUser user) {
|
||||
return AddItem(AuditLogEventType.UserDeleted, user.Id, new Dictionary<string, object?> {
|
||||
{ "username", user.UserName }
|
||||
public Task AddUserDeletedEvent(UserEntity user) {
|
||||
return AddItem(AuditLogEventType.UserDeleted, user.UserGuid.ToString(), new Dictionary<string, object?> {
|
||||
{ "username", user.Name }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -11,21 +11,19 @@ namespace Phantom.Server.Services.Audit;
|
||||
public sealed partial class AuditLog {
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly DatabaseProvider databaseProvider;
|
||||
private readonly IdentityLookup identityLookup;
|
||||
private readonly AuthenticationStateProvider authenticationStateProvider;
|
||||
private readonly TaskManager taskManager;
|
||||
|
||||
public AuditLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, IdentityLookup identityLookup, AuthenticationStateProvider authenticationStateProvider, TaskManager taskManager) {
|
||||
public AuditLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, AuthenticationStateProvider authenticationStateProvider, TaskManager taskManager) {
|
||||
this.cancellationToken = serviceConfiguration.CancellationToken;
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.identityLookup = identityLookup;
|
||||
this.authenticationStateProvider = authenticationStateProvider;
|
||||
this.taskManager = taskManager;
|
||||
}
|
||||
|
||||
private async Task<string?> GetCurrentAuthenticatedUserId() {
|
||||
private async Task<Guid?> GetCurrentAuthenticatedUserId() {
|
||||
var authenticationState = await authenticationStateProvider.GetAuthenticationStateAsync();
|
||||
return identityLookup.GetAuthenticatedUserId(authenticationState.User);
|
||||
return UserManager.GetAuthenticatedUserId(authenticationState.User);
|
||||
}
|
||||
|
||||
private async Task AddEntityToDatabase(AuditLogEntity logEntity) {
|
||||
@ -34,8 +32,8 @@ public sealed partial class AuditLog {
|
||||
await scope.Ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void AddItem(string? userId, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
|
||||
var logEntity = new AuditLogEntity(userId, eventType, subjectId, extra);
|
||||
private void AddItem(Guid? userGuid, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
|
||||
var logEntity = new AuditLogEntity(userGuid, eventType, subjectId, extra);
|
||||
taskManager.Run("Store audit log item to database", () => AddEntityToDatabase(logEntity));
|
||||
}
|
||||
|
||||
@ -50,7 +48,7 @@ public sealed partial class AuditLog {
|
||||
.AsQueryable()
|
||||
.OrderByDescending(static entity => entity.UtcTime)
|
||||
.Take(count)
|
||||
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserId, entity.User == null ? null : entity.User.UserName, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
|
||||
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,4 @@ using Phantom.Server.Database.Enums;
|
||||
|
||||
namespace Phantom.Server.Services.Audit;
|
||||
|
||||
public sealed record AuditLogItem(DateTime UtcTime, string? UserId, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data);
|
||||
public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data);
|
||||
|
8
Server/Phantom.Server.Services/Users/AddRoleError.cs
Normal file
8
Server/Phantom.Server.Services/Users/AddRoleError.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public enum AddRoleError : byte {
|
||||
NameIsEmpty,
|
||||
NameIsTooLong,
|
||||
NameAlreadyExists,
|
||||
UnknownError
|
||||
}
|
29
Server/Phantom.Server.Services/Users/AddUserError.cs
Normal file
29
Server/Phantom.Server.Services/Users/AddUserError.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public abstract record AddUserError {
|
||||
private AddUserError() {}
|
||||
|
||||
public sealed record NameIsEmpty : AddUserError;
|
||||
|
||||
public sealed record NameIsTooLong(int MaximumLength) : AddUserError;
|
||||
|
||||
public sealed record NameAlreadyExists : AddUserError;
|
||||
|
||||
public sealed record PasswordIsInvalid(ImmutableArray<PasswordRequirementViolation> Violations) : AddUserError;
|
||||
|
||||
public sealed record UnknownError : AddUserError;
|
||||
}
|
||||
|
||||
public static class AddUserErrorExtensions {
|
||||
public static string ToSentences(this AddUserError error, string delimiter) {
|
||||
return error switch {
|
||||
AddUserError.NameIsEmpty => "Name cannot be empty.",
|
||||
AddUserError.NameIsTooLong e => "Name cannot be longer than " + e.MaximumLength + " character(s).",
|
||||
AddUserError.NameAlreadyExists => "Name is already occupied.",
|
||||
AddUserError.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
}
|
7
Server/Phantom.Server.Services/Users/DeleteUserResult.cs
Normal file
7
Server/Phantom.Server.Services/Users/DeleteUserResult.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public enum DeleteUserResult : byte {
|
||||
Deleted,
|
||||
NotFound,
|
||||
Failed
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public sealed class IdentityLookup {
|
||||
private readonly UserManager<IdentityUser> userManager;
|
||||
|
||||
public IdentityLookup(UserManager<IdentityUser> userManager) {
|
||||
this.userManager = userManager;
|
||||
}
|
||||
|
||||
public string? GetAuthenticatedUserId(ClaimsPrincipal user) {
|
||||
return user.Identity is { IsAuthenticated: true } ? userManager.GetUserId(user) : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public abstract record PasswordRequirementViolation {
|
||||
private PasswordRequirementViolation() {}
|
||||
|
||||
public sealed record TooShort(int MinimumLength) : PasswordRequirementViolation;
|
||||
|
||||
public sealed record LowercaseLetterRequired : PasswordRequirementViolation;
|
||||
|
||||
public sealed record UppercaseLetterRequired : PasswordRequirementViolation;
|
||||
|
||||
public sealed record DigitRequired : PasswordRequirementViolation;
|
||||
}
|
||||
|
||||
public static class PasswordRequirementViolationExtensions {
|
||||
public static string ToSentence(this PasswordRequirementViolation violation) {
|
||||
return violation switch {
|
||||
PasswordRequirementViolation.TooShort v => "Password must be at least " + v.MinimumLength + " character(s) long.",
|
||||
PasswordRequirementViolation.LowercaseLetterRequired => "Password must contain a lowercase letter.",
|
||||
PasswordRequirementViolation.UppercaseLetterRequired => "Password must contain an uppercase letter.",
|
||||
PasswordRequirementViolation.DigitRequired => "Password must contain a digit.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
}
|
60
Server/Phantom.Server.Services/Users/RoleManager.cs
Normal file
60
Server/Phantom.Server.Services/Users/RoleManager.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public sealed class RoleManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();
|
||||
|
||||
private const int MaxRoleNameLength = 40;
|
||||
|
||||
private readonly ApplicationDbContext db;
|
||||
|
||||
public RoleManager(ApplicationDbContext db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public Task<List<RoleEntity>> GetAll() {
|
||||
return db.Roles.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<ImmutableHashSet<string>> GetAllNames() {
|
||||
return db.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
|
||||
}
|
||||
|
||||
public ValueTask<RoleEntity?> GetByGuid(Guid guid) {
|
||||
return db.Roles.FindAsync(guid);
|
||||
}
|
||||
|
||||
public async Task<Result<RoleEntity, AddRoleError>> Create(Guid guid, string name) {
|
||||
if (string.IsNullOrWhiteSpace(name)) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsEmpty);
|
||||
}
|
||||
else if (name.Length > MaxRoleNameLength) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsTooLong);
|
||||
}
|
||||
|
||||
try {
|
||||
if (await db.Roles.AnyAsync(role => role.Name == name)) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameAlreadyExists);
|
||||
}
|
||||
|
||||
var role = new RoleEntity(guid, name);
|
||||
|
||||
db.Roles.Add(role);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Logger.Information("Created role \"{Name}\" (GUID {Guid}).", name, guid);
|
||||
return Result.Ok<RoleEntity, AddRoleError>(role);
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create role \"{Name}\" (GUID {Guid}).", name, guid);
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.UnknownError);
|
||||
}
|
||||
}
|
||||
}
|
23
Server/Phantom.Server.Services/Users/SetUserPasswordError.cs
Normal file
23
Server/Phantom.Server.Services/Users/SetUserPasswordError.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public abstract record SetUserPasswordError {
|
||||
private SetUserPasswordError() {}
|
||||
|
||||
public sealed record UserNotFound : SetUserPasswordError;
|
||||
|
||||
public sealed record PasswordIsInvalid(ImmutableArray<PasswordRequirementViolation> Violations) : SetUserPasswordError;
|
||||
|
||||
public sealed record UnknownError : SetUserPasswordError;
|
||||
}
|
||||
|
||||
public static class SetUserPasswordErrorExtensions {
|
||||
public static string ToSentences(this SetUserPasswordError error, string delimiter) {
|
||||
return error switch {
|
||||
SetUserPasswordError.UserNotFound => "User not found.",
|
||||
SetUserPasswordError.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
}
|
148
Server/Phantom.Server.Services/Users/UserManager.cs
Normal file
148
Server/Phantom.Server.Services/Users/UserManager.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public sealed class UserManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<UserManager>();
|
||||
|
||||
private const int MaxUserNameLength = 40;
|
||||
|
||||
private readonly ApplicationDbContext db;
|
||||
|
||||
public UserManager(ApplicationDbContext db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public static Guid? GetAuthenticatedUserId(ClaimsPrincipal user) {
|
||||
if (user.Identity is not { IsAuthenticated: true }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var claim = user.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (claim == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Guid.TryParse(claim.Value, out var guid) ? guid : null;
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<UserEntity>> GetAll() {
|
||||
return db.Users.AsAsyncEnumerable().ToImmutableArrayAsync();
|
||||
}
|
||||
|
||||
public Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) {
|
||||
return db.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<UserEntity?> GetByName(string username) {
|
||||
return db.Users.FirstOrDefaultAsync(user => user.Name == username);
|
||||
}
|
||||
|
||||
public async Task<UserEntity?> GetAuthenticated(string username, string password) {
|
||||
var user = await db.Users.FirstOrDefaultAsync(user => user.Name == username);
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (UserPasswords.Verify(user, password)) {
|
||||
case PasswordVerificationResult.SuccessRehashNeeded:
|
||||
try {
|
||||
UserPasswords.Set(user, password);
|
||||
await db.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Warning(e, "Could not rehash password for \"{Username}\".", user.Name);
|
||||
}
|
||||
|
||||
goto case PasswordVerificationResult.Success;
|
||||
|
||||
case PasswordVerificationResult.Success:
|
||||
return user;
|
||||
|
||||
case PasswordVerificationResult.Failed:
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) {
|
||||
if (string.IsNullOrWhiteSpace(username)) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameIsEmpty());
|
||||
}
|
||||
else if (username.Length > MaxUserNameLength) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameIsTooLong(MaxUserNameLength));
|
||||
}
|
||||
|
||||
var requirementViolations = UserPasswords.CheckRequirements(password);
|
||||
if (!requirementViolations.IsEmpty) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.PasswordIsInvalid(requirementViolations));
|
||||
}
|
||||
|
||||
try {
|
||||
if (await db.Users.AnyAsync(user => user.Name == username)) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameAlreadyExists());
|
||||
}
|
||||
|
||||
var guid = Guid.NewGuid();
|
||||
var user = new UserEntity(guid, username);
|
||||
UserPasswords.Set(user, password);
|
||||
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, guid);
|
||||
return Result.Ok<UserEntity, AddUserError>(user);
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create user \"{Name}\".", username);
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.UnknownError());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<SetUserPasswordError>> SetUserPassword(Guid guid, string password) {
|
||||
var user = await db.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UserNotFound());
|
||||
}
|
||||
|
||||
try {
|
||||
var requirementViolations = UserPasswords.CheckRequirements(password);
|
||||
if (!requirementViolations.IsEmpty) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.PasswordIsInvalid(requirementViolations));
|
||||
}
|
||||
|
||||
UserPasswords.Set(user, password);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Logger.Information("Changed password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return Result.Ok<SetUserPasswordError>();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not change password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UnknownError());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DeleteUserResult> DeleteByGuid(Guid guid) {
|
||||
var user = await db.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return DeleteUserResult.NotFound;
|
||||
}
|
||||
|
||||
try {
|
||||
db.Users.Remove(user);
|
||||
await db.SaveChangesAsync();
|
||||
return DeleteUserResult.Deleted;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not delete user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return DeleteUserResult.Failed;
|
||||
}
|
||||
}
|
||||
}
|
41
Server/Phantom.Server.Services/Users/UserPasswords.cs
Normal file
41
Server/Phantom.Server.Services/Users/UserPasswords.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Server.Database.Entities;
|
||||
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
internal static class UserPasswords {
|
||||
private static PasswordHasher<UserEntity> Hasher { get; } = new ();
|
||||
|
||||
private const int MinimumLength = 16;
|
||||
|
||||
public static ImmutableArray<PasswordRequirementViolation> CheckRequirements(string password) {
|
||||
var violations = ImmutableArray.CreateBuilder<PasswordRequirementViolation>();
|
||||
|
||||
if (password.Length < MinimumLength) {
|
||||
violations.Add(new PasswordRequirementViolation.TooShort(MinimumLength));
|
||||
}
|
||||
|
||||
if (!password.Any(char.IsLower)) {
|
||||
violations.Add(new PasswordRequirementViolation.LowercaseLetterRequired());
|
||||
}
|
||||
|
||||
if (!password.Any(char.IsUpper)) {
|
||||
violations.Add(new PasswordRequirementViolation.UppercaseLetterRequired());
|
||||
}
|
||||
|
||||
if (!password.Any(char.IsDigit)) {
|
||||
violations.Add(new PasswordRequirementViolation.DigitRequired());
|
||||
}
|
||||
|
||||
return violations.ToImmutable();
|
||||
}
|
||||
|
||||
public static void Set(UserEntity user, string password) {
|
||||
user.PasswordHash = Hasher.HashPassword(user, password);
|
||||
}
|
||||
|
||||
public static PasswordVerificationResult Verify(UserEntity user, string password) {
|
||||
return Hasher.VerifyHashedPassword(user, user.PasswordHash, password);
|
||||
}
|
||||
}
|
76
Server/Phantom.Server.Services/Users/UserRoleManager.cs
Normal file
76
Server/Phantom.Server.Services/Users/UserRoleManager.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Server.Services.Users;
|
||||
|
||||
public sealed class UserRoleManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
|
||||
|
||||
private readonly ApplicationDbContext db;
|
||||
|
||||
public UserRoleManager(ApplicationDbContext db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
|
||||
return db.UserRoles
|
||||
.Include(static ur => ur.Role)
|
||||
.GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
|
||||
.ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
|
||||
return db.UserRoles
|
||||
.Include(static ur => ur.Role)
|
||||
.Where(ur => ur.UserGuid == user.UserGuid)
|
||||
.Select(static ur => ur.Role)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableArrayAsync();
|
||||
}
|
||||
|
||||
public Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
|
||||
return db.UserRoles
|
||||
.Where(ur => ur.UserGuid == user.UserGuid)
|
||||
.Select(static ur => ur.RoleGuid)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableSetAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Add(UserEntity user, RoleEntity role) {
|
||||
try {
|
||||
var userRole = await db.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
|
||||
if (userRole == null) {
|
||||
userRole = new UserRoleEntity(user.UserGuid, role.RoleGuid);
|
||||
db.UserRoles.Add(userRole);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
Logger.Information("Added user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not add user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Remove(UserEntity user, RoleEntity role) {
|
||||
try {
|
||||
var userRole = await db.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
|
||||
if (userRole != null) {
|
||||
db.UserRoles.Remove(userRole);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
Logger.Information("Removed user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not remove user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Server.Services.Users;
|
||||
using Phantom.Server.Web.Identity.Interfaces;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using ILogger = Serilog.ILogger;
|
||||
@ -13,67 +15,63 @@ public sealed class PhantomLoginManager {
|
||||
public static bool IsAuthenticated(ClaimsPrincipal user) {
|
||||
return user.Identity is { IsAuthenticated: true };
|
||||
}
|
||||
|
||||
internal static string? GetAuthenticatedUserId(ClaimsPrincipal user, UserManager<IdentityUser> userManager) {
|
||||
return IsAuthenticated(user) ? userManager.GetUserId(user) : null;
|
||||
}
|
||||
|
||||
|
||||
private readonly INavigation navigation;
|
||||
private readonly UserManager userManager;
|
||||
private readonly PhantomLoginStore loginStore;
|
||||
private readonly UserManager<IdentityUser> userManager;
|
||||
private readonly SignInManager<IdentityUser> signInManager;
|
||||
private readonly ILoginEvents loginEvents;
|
||||
|
||||
public PhantomLoginManager(INavigation navigation, PhantomLoginStore loginStore, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, ILoginEvents loginEvents) {
|
||||
public PhantomLoginManager(INavigation navigation, UserManager userManager, PhantomLoginStore loginStore, ILoginEvents loginEvents) {
|
||||
this.navigation = navigation;
|
||||
this.loginStore = loginStore;
|
||||
this.userManager = userManager;
|
||||
this.signInManager = signInManager;
|
||||
this.loginStore = loginStore;
|
||||
this.loginEvents = loginEvents;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> SignIn(string username, string password, string? returnUrl = null) {
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
if (user == null) {
|
||||
return SignInResult.Failed;
|
||||
public async Task<bool> SignIn(string username, string password, string? returnUrl = null) {
|
||||
if (await userManager.GetAuthenticated(username, password) == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: true);
|
||||
if (result == SignInResult.Success) {
|
||||
Logger.Debug("Created login token for {Username}.", username);
|
||||
|
||||
string token = TokenGenerator.Create(60);
|
||||
loginStore.Add(token, user, password, returnUrl ?? string.Empty);
|
||||
navigation.NavigateTo("login" + QueryString.Create("token", token), forceLoad: true);
|
||||
}
|
||||
|
||||
return result;
|
||||
Logger.Debug("Created login token for {Username}.", username);
|
||||
|
||||
string token = TokenGenerator.Create(60);
|
||||
loginStore.Add(token, username, password, returnUrl ?? string.Empty);
|
||||
navigation.NavigateTo("login" + QueryString.Create("token", token), forceLoad: true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal async Task SignOut() {
|
||||
if (GetAuthenticatedUserId(signInManager.Context.User, userManager) is {} userId) {
|
||||
loginEvents.UserLoggedOut(userId);
|
||||
}
|
||||
|
||||
await signInManager.SignOutAsync();
|
||||
}
|
||||
|
||||
internal async Task<string?> ProcessTokenAndGetReturnUrl(string token) {
|
||||
internal async Task<SignInResult?> ProcessToken(string token) {
|
||||
var entry = loginStore.Pop(token);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = entry.User;
|
||||
var result = await signInManager.PasswordSignInAsync(user, entry.Password, lockoutOnFailure: false, isPersistent: true);
|
||||
if (result == SignInResult.Success) {
|
||||
Logger.Information("Successful login for {Username}.", user.UserName);
|
||||
loginEvents.UserLoggedIn(user.Id);
|
||||
return entry.ReturnUrl;
|
||||
}
|
||||
else {
|
||||
Logger.Warning("Error logging in {Username}: {Result}.", user.UserName, result);
|
||||
var user = await userManager.GetAuthenticated(entry.Username, entry.Password);
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.Information("Successful login for {Username}.", user.Name);
|
||||
loginEvents.UserLoggedIn(user);
|
||||
|
||||
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserGuid.ToString()));
|
||||
|
||||
var authenticationProperties = new AuthenticationProperties {
|
||||
IsPersistent = true
|
||||
};
|
||||
|
||||
return new SignInResult(new ClaimsPrincipal(identity), authenticationProperties, entry.ReturnUrl);
|
||||
}
|
||||
|
||||
internal sealed record SignInResult(ClaimsPrincipal ClaimsPrincipal, AuthenticationProperties AuthenticationProperties, string ReturnUrl);
|
||||
|
||||
internal void OnSignedOut(ClaimsPrincipal user) {
|
||||
if (UserManager.GetAuthenticatedUserId(user) is {} userGuid) {
|
||||
loginEvents.UserLoggedOut(userGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
@ -30,7 +29,7 @@ public sealed class PhantomLoginStore {
|
||||
|
||||
foreach (var (token, entry) in loginEntries) {
|
||||
if (entry.IsExpired) {
|
||||
Logger.Debug("Expired login entry for {Username}.", entry.User.UserName);
|
||||
Logger.Debug("Expired login entry for {Username}.", entry.Username);
|
||||
loginEntries.TryRemove(token, out _);
|
||||
}
|
||||
}
|
||||
@ -40,8 +39,8 @@ public sealed class PhantomLoginStore {
|
||||
}
|
||||
}
|
||||
|
||||
internal void Add(string token, IdentityUser user, string password, string returnUrl) {
|
||||
loginEntries[token] = new LoginEntry(user, password, returnUrl, Stopwatch.StartNew());
|
||||
internal void Add(string token, string username, string password, string returnUrl) {
|
||||
loginEntries[token] = new LoginEntry(username, password, returnUrl, Stopwatch.StartNew());
|
||||
}
|
||||
|
||||
internal LoginEntry? Pop(string token) {
|
||||
@ -50,14 +49,14 @@ public sealed class PhantomLoginStore {
|
||||
}
|
||||
|
||||
if (entry.IsExpired) {
|
||||
Logger.Debug("Expired login entry for {Username}.", entry.User.UserName);
|
||||
Logger.Debug("Expired login entry for {Username}.", entry.Username);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
internal sealed record LoginEntry(IdentityUser User, string Password, string ReturnUrl, Stopwatch AddedTime) {
|
||||
internal sealed record LoginEntry(string Username, string Password, string ReturnUrl, Stopwatch AddedTime) {
|
||||
public bool IsExpired => AddedTime.Elapsed >= ExpirationTime;
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Phantom.Server.Web.Identity.Authentication;
|
||||
|
||||
sealed class RevalidatingIdentityAuthenticationStateProvider<TUser> : RevalidatingServerAuthenticationStateProvider where TUser : class {
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
private readonly IdentityOptions options;
|
||||
|
||||
public RevalidatingIdentityAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory, IOptions<IdentityOptions> optionsAccessor) : base(loggerFactory) {
|
||||
this.scopeFactory = scopeFactory;
|
||||
this.options = optionsAccessor.Value;
|
||||
}
|
||||
|
||||
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
|
||||
|
||||
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken) {
|
||||
// Get the user manager from a new scope to ensure it fetches fresh data
|
||||
var scope = scopeFactory.CreateScope();
|
||||
try {
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
|
||||
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
|
||||
} finally {
|
||||
if (scope is IAsyncDisposable asyncDisposable) {
|
||||
await asyncDisposable.DisposeAsync();
|
||||
}
|
||||
else {
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal) {
|
||||
var user = await userManager.GetUserAsync(principal);
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
else if (!userManager.SupportsUserSecurityStamp) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
var principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType);
|
||||
var userStamp = await userManager.GetSecurityStampAsync(user);
|
||||
return principalStamp == userStamp;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +1,26 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Web.Identity.Authentication;
|
||||
using Phantom.Server.Services.Users;
|
||||
using Phantom.Server.Web.Identity.Data;
|
||||
|
||||
namespace Phantom.Server.Web.Identity.Authorization;
|
||||
|
||||
public sealed class PermissionManager {
|
||||
private readonly DatabaseProvider databaseProvider;
|
||||
private readonly UserManager<IdentityUser> userManager;
|
||||
private readonly Dictionary<string, IdentityPermissions> userIdsToPermissionIds = new ();
|
||||
private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();
|
||||
|
||||
public PermissionManager(DatabaseProvider databaseProvider, UserManager<IdentityUser> userManager) {
|
||||
public PermissionManager(DatabaseProvider databaseProvider) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.userManager = userManager;
|
||||
}
|
||||
|
||||
private IdentityPermissions FetchPermissionsForUserId(string userId) {
|
||||
private IdentityPermissions FetchPermissionsForUserId(Guid userId) {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
var userPermissions = scope.Ctx.UserPermissions.Where(up => up.UserId == userId).Select(static up => up.PermissionId);
|
||||
var rolePermissions = scope.Ctx.UserRoles.Where(ur => ur.UserId == userId).Join(scope.Ctx.RolePermissions, static ur => ur.RoleId, static rp => rp.RoleId, static (ur, rp) => rp.PermissionId);
|
||||
var userPermissions = scope.Ctx.UserPermissions.Where(up => up.UserGuid == userId).Select(static up => up.PermissionId);
|
||||
var rolePermissions = scope.Ctx.UserRoles.Where(ur => ur.UserGuid == userId).Join(scope.Ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
|
||||
return new IdentityPermissions(userPermissions.Union(rolePermissions));
|
||||
}
|
||||
|
||||
private IdentityPermissions GetPermissionsForUserId(string userId, bool refreshCache) {
|
||||
private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) {
|
||||
if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) {
|
||||
return userPermissions;
|
||||
}
|
||||
@ -33,8 +30,8 @@ public sealed class PermissionManager {
|
||||
}
|
||||
|
||||
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
|
||||
string? userId = PhantomLoginManager.GetAuthenticatedUserId(user, userManager);
|
||||
return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId, refreshCache);
|
||||
Guid? userId = UserManager.GetAuthenticatedUserId(user);
|
||||
return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache);
|
||||
}
|
||||
|
||||
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
|
||||
|
@ -2,16 +2,20 @@
|
||||
|
||||
namespace Phantom.Server.Web.Identity.Data;
|
||||
|
||||
public sealed record Role(string Name, ImmutableArray<Permission> Permissions) {
|
||||
public sealed record Role(Guid Guid, string Name, ImmutableArray<Permission> Permissions) {
|
||||
private static readonly List<Role> AllRoles = new ();
|
||||
internal static IEnumerable<Role> All => AllRoles;
|
||||
|
||||
private static Role Register(string name, ImmutableArray<Permission> permissions) {
|
||||
var role = new Role(name, permissions);
|
||||
private static Role Register(Guid guid, string name, ImmutableArray<Permission> permissions) {
|
||||
var role = new Role(guid, name, permissions);
|
||||
AllRoles.Add(role);
|
||||
return role;
|
||||
}
|
||||
|
||||
public static readonly Role Administrator = Register("Administrator", Permission.All.ToImmutableArray());
|
||||
public static readonly Role InstanceManager = Register("Instance Manager", ImmutableArray.Create(Permission.ViewInstances, Permission.ViewInstanceLogs, Permission.CreateInstances, Permission.ControlInstances, Permission.ViewEvents));
|
||||
private static Guid SystemRoleGuid(byte id) {
|
||||
return new Guid(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, id);
|
||||
}
|
||||
|
||||
public static readonly Role Administrator = Register(SystemRoleGuid(1), "Administrator", Permission.All.ToImmutableArray());
|
||||
public static readonly Role InstanceManager = Register(SystemRoleGuid(2), "Instance Manager", ImmutableArray.Create(Permission.ViewInstances, Permission.ViewInstanceLogs, Permission.CreateInstances, Permission.ControlInstances, Permission.ViewEvents));
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
namespace Phantom.Server.Web.Identity.Interfaces;
|
||||
using Phantom.Server.Database.Entities;
|
||||
|
||||
namespace Phantom.Server.Web.Identity.Interfaces;
|
||||
|
||||
public interface ILoginEvents {
|
||||
void UserLoggedIn(string userId);
|
||||
void UserLoggedOut(string userId);
|
||||
void UserLoggedIn(UserEntity user);
|
||||
void UserLoggedOut(Guid userGuid);
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Server.Database\Phantom.Server.Database.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Server.Services\Phantom.Server.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,10 +1,13 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Database.Entities;
|
||||
using Phantom.Server.Services.Users;
|
||||
using Phantom.Server.Web.Identity.Data;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Runtime;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Server.Web.Identity;
|
||||
@ -18,22 +21,22 @@ public sealed class PhantomIdentityConfigurator {
|
||||
}
|
||||
|
||||
private readonly ApplicationDbContext db;
|
||||
private readonly RoleManager<IdentityRole> roleManager;
|
||||
private readonly RoleManager roleManager;
|
||||
|
||||
public PhantomIdentityConfigurator(ApplicationDbContext db, RoleManager<IdentityRole> roleManager) {
|
||||
public PhantomIdentityConfigurator(ApplicationDbContext db, RoleManager roleManager) {
|
||||
this.db = db;
|
||||
this.roleManager = roleManager;
|
||||
}
|
||||
|
||||
private async Task Initialize() {
|
||||
CreatePermissions();
|
||||
await CreatePermissions();
|
||||
await CreateDefaultRoles();
|
||||
await AssignDefaultRolePermissions();
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private void CreatePermissions() {
|
||||
var existingPermissionIds = db.Permissions.Select(static p => p.Id).ToHashSet();
|
||||
private async Task CreatePermissions() {
|
||||
var existingPermissionIds = await db.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync();
|
||||
var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds);
|
||||
|
||||
if (!missingPermissionIds.IsEmpty) {
|
||||
@ -45,55 +48,65 @@ public sealed class PhantomIdentityConfigurator {
|
||||
}
|
||||
|
||||
private async Task CreateDefaultRoles() {
|
||||
foreach (var role in Role.All) {
|
||||
string name = role.Name;
|
||||
if (await roleManager.RoleExistsAsync(name)) {
|
||||
Logger.Information("Creating default roles.");
|
||||
|
||||
var allRoleNames = await roleManager.GetAllNames();
|
||||
|
||||
foreach (var (guid, name, _) in Role.All) {
|
||||
if (allRoleNames.Contains(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.Information("Creating default role {RoleName}.", name);
|
||||
var result = await roleManager.CreateAsync(new IdentityRole(name));
|
||||
|
||||
if (!result.Succeeded) {
|
||||
bool anyError = false;
|
||||
|
||||
foreach (var error in result.Errors) {
|
||||
Logger.Fatal("Error creating default role {RoleName}: {Error}", name, error.Description);
|
||||
anyError = true;
|
||||
|
||||
var result = await roleManager.Create(guid, name);
|
||||
if (result is Result<RoleEntity, AddRoleError>.Fail fail) {
|
||||
switch (fail.Error) {
|
||||
case AddRoleError.NameIsEmpty:
|
||||
Logger.Fatal("Error creating default role \"{Name}\", name is empty!", name);
|
||||
throw StopProcedureException.Instance;
|
||||
|
||||
case AddRoleError.NameIsTooLong:
|
||||
Logger.Fatal("Error creating default role \"{Name}\", name is too long!", name);
|
||||
throw StopProcedureException.Instance;
|
||||
|
||||
case AddRoleError.NameAlreadyExists:
|
||||
Logger.Warning("Error creating default role \"{Name}\", a role with this name already exists!", name);
|
||||
throw StopProcedureException.Instance;
|
||||
|
||||
default:
|
||||
Logger.Fatal("Error creating default role \"{Name}\", unknown error!", name);
|
||||
throw StopProcedureException.Instance;
|
||||
}
|
||||
|
||||
if (!anyError) {
|
||||
Logger.Fatal("Error creating default role {RoleName} due to unknown error.", name);
|
||||
}
|
||||
|
||||
throw StopProcedureException.Instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AssignDefaultRolePermissions() {
|
||||
Logger.Information("Assigning default role permissions..");
|
||||
Logger.Information("Assigning default role permissions.");
|
||||
|
||||
foreach (var role in Role.All) {
|
||||
var roleEntity = await roleManager.FindByNameAsync(role.Name);
|
||||
var roleEntity = await roleManager.GetByGuid(role.Guid);
|
||||
if (roleEntity == null) {
|
||||
Logger.Fatal("Error assigning default role permissions, role {RoleName} not found.", role.Name);
|
||||
Logger.Fatal("Error assigning default role permissions, role \"{Name}\" with GUID {Guid} not found.", role.Name, role.Guid);
|
||||
throw StopProcedureException.Instance;
|
||||
}
|
||||
|
||||
var existingPermissionIds = db.RolePermissions.Where(rp => rp.RoleId == roleEntity.Id).Select(static rp => rp.PermissionId).ToHashSet();
|
||||
var existingPermissionIds = await db.RolePermissions
|
||||
.Where(rp => rp.RoleGuid == roleEntity.RoleGuid)
|
||||
.Select(static rp => rp.PermissionId)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableSetAsync();
|
||||
|
||||
var missingPermissionIds = GetMissingPermissionsOrdered(role.Permissions, existingPermissionIds);
|
||||
|
||||
if (!missingPermissionIds.IsEmpty) {
|
||||
Logger.Information("Assigning default permission to role {RoleName}: {Permissions}", role.Name, string.Join(", ", missingPermissionIds));
|
||||
Logger.Information("Assigning default permission to role \"{Name}\": {Permissions}", role.Name, string.Join(", ", missingPermissionIds));
|
||||
foreach (var permissionId in missingPermissionIds) {
|
||||
db.RolePermissions.Add(new RolePermissionEntity(roleEntity.Id, permissionId));
|
||||
db.RolePermissions.Add(new RolePermissionEntity(roleEntity.RoleGuid, permissionId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, HashSet<string> existingPermissionIds) {
|
||||
private static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
|
||||
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Server.Database;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Phantom.Server.Services.Users;
|
||||
using Phantom.Server.Web.Identity.Authentication;
|
||||
using Phantom.Server.Web.Identity.Authorization;
|
||||
using Phantom.Server.Web.Identity.Data;
|
||||
@ -10,10 +10,8 @@ using Phantom.Server.Web.Identity.Data;
|
||||
namespace Phantom.Server.Web.Identity;
|
||||
|
||||
public static class PhantomIdentityExtensions {
|
||||
public static void AddPhantomIdentity<TUser, TRole>(this IServiceCollection services, CancellationToken cancellationToken) where TUser : class where TRole : class {
|
||||
services.AddIdentity<TUser, TRole>(ConfigureIdentity).AddEntityFrameworkStores<ApplicationDbContext>();
|
||||
services.ConfigureApplicationCookie(ConfigureIdentityCookie);
|
||||
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
public static void AddPhantomIdentity(this IServiceCollection services, CancellationToken cancellationToken) {
|
||||
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(ConfigureIdentityCookie);
|
||||
services.AddAuthorization(ConfigureAuthorization);
|
||||
|
||||
services.AddSingleton(PhantomLoginStore.Create(cancellationToken));
|
||||
@ -21,8 +19,11 @@ public static class PhantomIdentityExtensions {
|
||||
|
||||
services.AddScoped<PhantomIdentityConfigurator>();
|
||||
services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();
|
||||
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<TUser>>();
|
||||
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
|
||||
|
||||
services.AddScoped<UserManager>();
|
||||
services.AddScoped<RoleManager>();
|
||||
services.AddScoped<UserRoleManager>();
|
||||
services.AddTransient<PermissionManager>();
|
||||
}
|
||||
|
||||
@ -32,23 +33,6 @@ public static class PhantomIdentityExtensions {
|
||||
application.UseWhen(PhantomIdentityMiddleware.AcceptsPath, static app => app.UseMiddleware<PhantomIdentityMiddleware>());
|
||||
}
|
||||
|
||||
private static void ConfigureIdentity(IdentityOptions o) {
|
||||
o.SignIn.RequireConfirmedAccount = false;
|
||||
o.SignIn.RequireConfirmedEmail = false;
|
||||
o.SignIn.RequireConfirmedPhoneNumber = false;
|
||||
|
||||
o.Password.RequireLowercase = true;
|
||||
o.Password.RequireUppercase = true;
|
||||
o.Password.RequireDigit = true;
|
||||
o.Password.RequiredLength = 16;
|
||||
|
||||
o.Lockout.AllowedForNewUsers = true;
|
||||
o.Lockout.MaxFailedAccessAttempts = 10;
|
||||
o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(1);
|
||||
|
||||
o.Stores.MaxLengthForKeys = 128;
|
||||
}
|
||||
|
||||
private static void ConfigureIdentityCookie(CookieAuthenticationOptions o) {
|
||||
o.Cookie.Name = "Phantom.Identity";
|
||||
o.Cookie.HttpOnly = true;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Phantom.Server.Web.Identity.Authentication;
|
||||
using Phantom.Server.Web.Identity.Interfaces;
|
||||
|
||||
@ -22,11 +23,13 @@ sealed class PhantomIdentityMiddleware {
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
public async Task InvokeAsync(HttpContext context, INavigation navigation, PhantomLoginManager loginManager) {
|
||||
var path = context.Request.Path;
|
||||
if (path == LoginPath && context.Request.Query.TryGetValue("token", out var tokens) && tokens[0] is {} token && await loginManager.ProcessTokenAndGetReturnUrl(token) is {} returnUrl) {
|
||||
context.Response.Redirect(navigation.BasePath + returnUrl);
|
||||
if (path == LoginPath && context.Request.Query.TryGetValue("token", out var tokens) && tokens[0] is {} token && await loginManager.ProcessToken(token) is {} result) {
|
||||
await context.SignInAsync(result.ClaimsPrincipal, result.AuthenticationProperties);
|
||||
context.Response.Redirect(navigation.BasePath + result.ReturnUrl);
|
||||
}
|
||||
else if (path == LogoutPath) {
|
||||
await loginManager.SignOut();
|
||||
loginManager.OnSignedOut(context.User);
|
||||
await context.SignOutAsync();
|
||||
context.Response.Redirect(navigation.BasePath);
|
||||
}
|
||||
else {
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Phantom.Server.Services.Audit;
|
||||
using Phantom.Server.Database.Entities;
|
||||
using Phantom.Server.Services.Audit;
|
||||
using Phantom.Server.Web.Identity.Interfaces;
|
||||
|
||||
namespace Phantom.Server.Web.Base;
|
||||
@ -10,11 +11,11 @@ sealed class LoginEvents : ILoginEvents {
|
||||
this.auditLog = auditLog;
|
||||
}
|
||||
|
||||
public void UserLoggedIn(string userId) {
|
||||
auditLog.AddUserLoggedInEvent(userId);
|
||||
public void UserLoggedIn(UserEntity user) {
|
||||
auditLog.AddUserLoggedInEvent(user);
|
||||
}
|
||||
|
||||
public void UserLoggedOut(string userId) {
|
||||
auditLog.AddUserLoggedOutEvent(userId);
|
||||
public void UserLoggedOut(Guid userGuid) {
|
||||
auditLog.AddUserLoggedOutEvent(userGuid);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Web.Base;
|
||||
@ -34,10 +33,10 @@ public static class Launcher {
|
||||
|
||||
builder.Services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(config.KeyFolderPath));
|
||||
|
||||
builder.Services.AddDbContextPool<ApplicationDbContext>(dbOptionsBuilder, poolSize: 64);
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(dbOptionsBuilder, ServiceLifetime.Transient);
|
||||
builder.Services.AddSingleton<DatabaseProvider>();
|
||||
|
||||
builder.Services.AddPhantomIdentity<IdentityUser, IdentityRole>(config.CancellationToken);
|
||||
builder.Services.AddPhantomIdentity(config.CancellationToken);
|
||||
builder.Services.AddScoped<ILoginEvents, LoginEvents>();
|
||||
|
||||
builder.Services.AddRazorPages(static options => options.RootDirectory = "/Layout");
|
||||
|
@ -1,15 +1,14 @@
|
||||
@page "/audit"
|
||||
@attribute [Authorize(Permission.ViewAuditPolicy)]
|
||||
@using Phantom.Server.Database.Enums
|
||||
@using Phantom.Server.Services.Audit
|
||||
@using Phantom.Server.Services.Instances
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Phantom.Server.Services.Users
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Server.Database.Enums
|
||||
@implements IDisposable
|
||||
@inject AuditLog AuditLog
|
||||
@inject InstanceManager InstanceManager
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
@inject UserManager UserManager
|
||||
|
||||
<h1>Audit Log</h1>
|
||||
|
||||
@ -33,7 +32,7 @@
|
||||
<td>
|
||||
@(logItem.UserName ?? "-")
|
||||
<br>
|
||||
<code class="text-uppercase">@logItem.UserId</code>
|
||||
<code class="text-uppercase">@logItem.UserGuid</code>
|
||||
</td>
|
||||
<td>@logItem.EventType.ToNiceString()</td>
|
||||
<td>
|
||||
@ -55,7 +54,7 @@
|
||||
|
||||
private CancellationTokenSource? initializationCancellationTokenSource;
|
||||
private AuditLogItem[] logItems = Array.Empty<AuditLogItem>();
|
||||
private Dictionary<string, string>? userNamesById;
|
||||
private Dictionary<Guid, string>? userNamesByGuid;
|
||||
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
@ -64,7 +63,7 @@
|
||||
|
||||
try {
|
||||
logItems = await AuditLog.GetItems(50, cancellationToken);
|
||||
userNamesById = await UserManager.Users.ToDictionaryAsync(static user => user.Id, static user => user.UserName ?? user.Id, cancellationToken);
|
||||
userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken);
|
||||
instanceNamesByGuid = InstanceManager.GetInstanceNames();
|
||||
} finally {
|
||||
initializationCancellationTokenSource.Dispose();
|
||||
@ -74,7 +73,7 @@
|
||||
private string? GetSubjectName(AuditLogSubjectType type, string id) {
|
||||
return type switch {
|
||||
AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
|
||||
AuditLogSubjectType.User => userNamesById != null && userNamesById.TryGetValue(id, out var name) ? name : null,
|
||||
AuditLogSubjectType.User => userNamesByGuid != null && userNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
@page "/login"
|
||||
@using Phantom.Server.Web.Identity.Interfaces
|
||||
@using Phantom.Server.Web.Identity.Authentication
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@attribute [AllowAnonymous]
|
||||
@inject INavigation Navigation
|
||||
@ -51,21 +50,9 @@
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
string? returnUrl = Navigation.GetQueryParameter("return", out var url) ? url : null;
|
||||
var result = await LoginManager.SignIn(form.Username, form.Password, returnUrl);
|
||||
if (result != SignInResult.Success) {
|
||||
form.SubmitModel.StopSubmitting(GetErrorMessage(result));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetErrorMessage(SignInResult result) {
|
||||
if (result == SignInResult.Failed) {
|
||||
return "Invalid username or password.";
|
||||
}
|
||||
else if (result == SignInResult.LockedOut) {
|
||||
return "Too many failed login attempts. Please try again later.";
|
||||
}
|
||||
else {
|
||||
return "Unknown error.";
|
||||
|
||||
if (!await LoginManager.SignIn(form.Username, form.Password, returnUrl)) {
|
||||
form.SubmitModel.StopSubmitting("Invalid username or password.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,19 @@
|
||||
@page "/setup"
|
||||
@using Phantom.Server.Services.Users
|
||||
@using Phantom.Utils.Tasks
|
||||
@using Phantom.Server.Database.Entities
|
||||
@using Phantom.Server.Services
|
||||
@using Phantom.Server.Services.Audit
|
||||
@using Phantom.Server.Web.Identity.Authentication
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Utils.Cryptography
|
||||
@using System.Security.Cryptography
|
||||
@attribute [AllowAnonymous]
|
||||
@inject ServiceConfiguration ServiceConfiguration
|
||||
@inject PhantomLoginManager LoginManager
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
@inject UserManager UserManager
|
||||
@inject RoleManager RoleManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<h1>Administrator Setup</h1>
|
||||
@ -62,15 +66,14 @@
|
||||
form.SubmitModel.StopSubmitting("Invalid administrator token.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createUserResult = await CreateOrUpdateAdministrator();
|
||||
if (!createUserResult.Succeeded) {
|
||||
form.SubmitModel.StopSubmitting(GetErrors(createUserResult));
|
||||
|
||||
if (await CreateOrUpdateAdministrator() is Result<string>.Fail fail) {
|
||||
form.SubmitModel.StopSubmitting(fail.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var signInResult = await LoginManager.SignIn(form.Username, form.Password);
|
||||
if (!signInResult.Succeeded) {
|
||||
if (!signInResult) {
|
||||
form.SubmitModel.StopSubmitting("Error logging in.");
|
||||
}
|
||||
}
|
||||
@ -86,36 +89,46 @@
|
||||
return CryptographicOperations.FixedTimeEquals(formTokenBytes, ServiceConfiguration.AdministratorToken);
|
||||
}
|
||||
|
||||
private async Task<IdentityResult> CreateOrUpdateAdministrator() {
|
||||
var existingUser = await UserManager.FindByNameAsync(form.Username);
|
||||
private async Task<Result<string>> CreateOrUpdateAdministrator() {
|
||||
var existingUser = await UserManager.GetByName(form.Username);
|
||||
return existingUser == null ? await CreateAdministrator() : await UpdateAdministrator(existingUser);
|
||||
}
|
||||
|
||||
private async Task<IdentityResult> CreateAdministrator() {
|
||||
var newUser = new IdentityUser(form.Username);
|
||||
var createUserResult = await UserManager.CreateAsync(newUser, form.Password);
|
||||
if (!createUserResult.Succeeded) {
|
||||
return createUserResult;
|
||||
private async Task<Result<string>> CreateAdministrator() {
|
||||
var administratorRole = await RoleManager.GetByGuid(Role.Administrator.Guid);
|
||||
if (administratorRole == null) {
|
||||
return Result.Fail("Administrator role not found.");
|
||||
}
|
||||
|
||||
await AuditLog.AddAdministratorUserCreatedEvent(newUser);
|
||||
|
||||
var addToRoleResult = await UserManager.AddToRoleAsync(newUser, Role.Administrator.Name);
|
||||
return addToRoleResult;
|
||||
}
|
||||
switch (await UserManager.CreateUser(form.Username, form.Password)) {
|
||||
case Result<UserEntity, AddUserError>.Ok ok:
|
||||
var administratorUser = ok.Value;
|
||||
await AuditLog.AddAdministratorUserCreatedEvent(administratorUser);
|
||||
|
||||
private async Task<IdentityResult> UpdateAdministrator(IdentityUser existingUser) {
|
||||
await UserManager.RemovePasswordAsync(existingUser);
|
||||
var result = await UserManager.AddPasswordAsync(existingUser, form.Password);
|
||||
if (result.Succeeded) {
|
||||
await AuditLog.AddAdministratorUserModifiedEvent(existingUser);
|
||||
if (!await UserRoleManager.Add(administratorUser, administratorRole)) {
|
||||
return Result.Fail("Could not assign administrator role to user.");
|
||||
}
|
||||
|
||||
return Result.Ok<string>();
|
||||
|
||||
case Result<UserEntity, AddUserError>.Fail fail:
|
||||
return Result.Fail(fail.Error.ToSentences("\n"));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
return Result.Fail("Unknown error.");
|
||||
}
|
||||
|
||||
private string GetErrors(IdentityResult result) {
|
||||
return string.Join("\n", result.Errors.Select(static error => error.Description));
|
||||
private async Task<Result<string>> UpdateAdministrator(UserEntity existingUser) {
|
||||
switch (await UserManager.SetUserPassword(existingUser.UserGuid, form.Password)) {
|
||||
case Result<SetUserPasswordError>.Ok:
|
||||
await AuditLog.AddAdministratorUserModifiedEvent(existingUser);
|
||||
return Result.Ok<string>();
|
||||
|
||||
case Result<SetUserPasswordError>.Fail fail:
|
||||
return Result.Fail(fail.Error.ToSentences("\n"));
|
||||
}
|
||||
|
||||
return Result.Fail("Unknown error.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
@page "/users"
|
||||
@using System.Collections.Immutable
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Phantom.Server.Database.Entities
|
||||
@using Phantom.Server.Services.Users
|
||||
@using System.Collections.Immutable
|
||||
@attribute [Authorize(Permission.ViewUsersPolicy)]
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
@inject UserManager UserManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
@inject PermissionManager PermissionManager
|
||||
@inject IdentityLookup IdentityLookup
|
||||
|
||||
<h1>Users</h1>
|
||||
|
||||
@ -28,20 +28,20 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{ var myUserId = IdentityLookup.GetAuthenticatedUserId(context.User); }
|
||||
@{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); }
|
||||
@foreach (var user in allUsers) {
|
||||
var isMe = myUserId == user.Id;
|
||||
var isMe = myUserId == user.UserGuid;
|
||||
<tr>
|
||||
<td>
|
||||
<code class="text-uppercase">@user.Id</code>
|
||||
<code class="text-uppercase">@user.UserGuid</code>
|
||||
</td>
|
||||
@if (isMe) {
|
||||
<td class="fw-semibold">@user.UserName</td>
|
||||
<td class="fw-semibold">@user.Name</td>
|
||||
}
|
||||
else {
|
||||
<td>@user.UserName</td>
|
||||
<td>@user.Name</td>
|
||||
}
|
||||
<td>@(userRoles.TryGetValue(user.Id, out var roles) ? roles : "-")</td>
|
||||
<td>@(userGuidToRoleDescription.TryGetValue(user.UserGuid, out var roles) ? roles : "?")</td>
|
||||
@if (canEdit) {
|
||||
<td>
|
||||
@if (!isMe) {
|
||||
@ -65,42 +65,46 @@
|
||||
|
||||
@code {
|
||||
|
||||
private ImmutableArray<IdentityUser> allUsers = ImmutableArray<IdentityUser>.Empty;
|
||||
private readonly Dictionary<string, string> userRoles = new();
|
||||
private ImmutableArray<UserEntity> allUsers = ImmutableArray<UserEntity>.Empty;
|
||||
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new();
|
||||
|
||||
private UserRolesDialog userRolesDialog = null!;
|
||||
private UserDeleteDialog userDeleteDialog = null!;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
allUsers = UserManager.Users.OrderBy(static user => user.UserName).ToImmutableArray();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
var unsortedUsers = await UserManager.GetAll();
|
||||
allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name));
|
||||
|
||||
foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) {
|
||||
userGuidToRoleDescription[userGuid] = StringifyRoles(roles);
|
||||
}
|
||||
|
||||
foreach (var user in allUsers) {
|
||||
await RefreshUserRoles(user);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshUserRoles(IdentityUser user) {
|
||||
var roles = await UserManager.GetRolesAsync(user);
|
||||
if (roles.Count > 0) {
|
||||
userRoles[user.Id] = string.Join(", ", roles);
|
||||
}
|
||||
else {
|
||||
userRoles.Remove(user.Id);
|
||||
}
|
||||
private async Task RefreshUserRoles(UserEntity user) {
|
||||
var roles = await UserRoleManager.GetUserRoles(user);
|
||||
userGuidToRoleDescription[user.UserGuid] = StringifyRoles(roles);
|
||||
}
|
||||
|
||||
private void OnUserAdded(IdentityUser user) {
|
||||
private static string StringifyRoles(ImmutableArray<RoleEntity> roles) {
|
||||
return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name));
|
||||
}
|
||||
|
||||
private Task OnUserAdded(UserEntity user) {
|
||||
allUsers = allUsers.Add(user);
|
||||
return RefreshUserRoles(user);
|
||||
}
|
||||
|
||||
private async Task OnUserRolesChanged(IdentityUser user) {
|
||||
await RefreshUserRoles(user);
|
||||
private Task OnUserRolesChanged(UserEntity user) {
|
||||
return RefreshUserRoles(user);
|
||||
}
|
||||
|
||||
private void OnUserDeleted(IdentityUser user) {
|
||||
private void OnUserDeleted(UserEntity user) {
|
||||
allUsers = allUsers.Remove(user);
|
||||
userGuidToRoleDescription.Remove(user.UserGuid);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Phantom.Server.Services.Users
|
||||
@using Phantom.Server.Database.Entities
|
||||
@using Phantom.Server.Services.Audit
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Utils.Tasks
|
||||
@inherits PhantomComponent
|
||||
@inject IJSRuntime Js;
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
@inject UserManager UserManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Form Model="form" OnSubmit="AddUser">
|
||||
@ -37,7 +39,7 @@
|
||||
public string ModalId { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<IdentityUser> UserAdded { get; set; }
|
||||
public EventCallback<UserEntity> UserAdded { get; set; }
|
||||
|
||||
private readonly AddUserFormModel form = new();
|
||||
|
||||
@ -57,16 +59,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var user = new IdentityUser(form.Username);
|
||||
var result = await UserManager.CreateAsync(user, form.Password);
|
||||
if (result.Succeeded) {
|
||||
await AuditLog.AddUserCreatedEvent(user);
|
||||
await UserAdded.InvokeAsync(user);
|
||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||
form.SubmitModel.StopSubmitting();
|
||||
}
|
||||
else {
|
||||
form.SubmitModel.StopSubmitting(string.Join("\n", result.Errors.Select(static error => error.Description)));
|
||||
switch (await UserManager.CreateUser(form.Username, form.Password)) {
|
||||
case Result<UserEntity, AddUserError>.Ok ok:
|
||||
await AuditLog.AddUserCreatedEvent(ok.Value);
|
||||
await UserAdded.InvokeAsync(ok.Value);
|
||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||
form.SubmitModel.StopSubmitting();
|
||||
break;
|
||||
|
||||
case Result<UserEntity, AddUserError>.Fail fail:
|
||||
form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Phantom.Server.Database.Entities
|
||||
@using Phantom.Server.Services.Audit
|
||||
@using Phantom.Server.Services.Users
|
||||
@inherits UserEditDialogBase
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
@inject UserManager UserManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Modal Id="@ModalId" TitleText="Delete User">
|
||||
@ -18,14 +19,20 @@
|
||||
|
||||
@code {
|
||||
|
||||
protected override async Task DoEdit(IdentityUser user) {
|
||||
var result = await UserManager.DeleteAsync(user);
|
||||
if (result.Succeeded) {
|
||||
await AuditLog.AddUserDeletedEvent(user);
|
||||
await OnEditSuccess();
|
||||
}
|
||||
else {
|
||||
OnEditFailure(string.Join("\n", result.Errors.Select(static error => error.Description)));
|
||||
protected override async Task DoEdit(UserEntity user) {
|
||||
switch (await UserManager.DeleteByGuid(user.UserGuid)) {
|
||||
case DeleteUserResult.Deleted:
|
||||
await AuditLog.AddUserDeletedEvent(user);
|
||||
await OnEditSuccess();
|
||||
break;
|
||||
|
||||
case DeleteUserResult.NotFound:
|
||||
await OnEditSuccess();
|
||||
break;
|
||||
|
||||
case DeleteUserResult.Failed:
|
||||
OnEditFailure("Could not delete user.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.JSInterop;
|
||||
using Phantom.Server.Database.Entities;
|
||||
using Phantom.Server.Web.Base;
|
||||
using Phantom.Server.Web.Components.Forms;
|
||||
using Phantom.Server.Web.Identity.Data;
|
||||
@ -15,23 +15,23 @@ public abstract class UserEditDialogBase : PhantomComponent {
|
||||
public string ModalId { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<IdentityUser> UserModified { get; set; }
|
||||
public EventCallback<UserEntity> UserModified { get; set; }
|
||||
|
||||
protected readonly FormButtonSubmit.SubmitModel SubmitModel = new();
|
||||
|
||||
private IdentityUser? EditedUser { get; set; } = null;
|
||||
private UserEntity? EditedUser { get; set; } = null;
|
||||
protected string EditedUserName { get; private set; } = string.Empty;
|
||||
|
||||
internal async Task Show(IdentityUser user) {
|
||||
internal async Task Show(UserEntity user) {
|
||||
EditedUser = user;
|
||||
EditedUserName = user.UserName ?? $"<{user.Id}>";
|
||||
EditedUserName = user.Name;
|
||||
await BeforeShown(user);
|
||||
|
||||
StateHasChanged();
|
||||
await Js.InvokeVoidAsync("showModal", ModalId);
|
||||
}
|
||||
|
||||
protected virtual Task BeforeShown(IdentityUser user) {
|
||||
protected virtual Task BeforeShown(UserEntity user) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ public abstract class UserEditDialogBase : PhantomComponent {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task DoEdit(IdentityUser user);
|
||||
protected abstract Task DoEdit(UserEntity user);
|
||||
|
||||
protected async Task OnEditSuccess() {
|
||||
await UserModified.InvokeAsync(EditedUser);
|
||||
|
@ -1,10 +1,10 @@
|
||||
@using Phantom.Utils.Collections
|
||||
@using System.Collections.Immutable
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Phantom.Server.Database.Entities
|
||||
@using Phantom.Server.Services.Audit
|
||||
@using Phantom.Server.Services.Users
|
||||
@inherits UserEditDialogBase
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
@inject RoleManager<IdentityRole> RoleManager
|
||||
@inject UserManager UserManager
|
||||
@inject RoleManager RoleManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Modal Id="@ModalId" TitleText="Manage User Roles">
|
||||
@ -14,7 +14,7 @@
|
||||
var item = items[index];
|
||||
<div class="mt-1">
|
||||
<input id="role-@index" type="checkbox" class="form-check-input" @bind="@item.Checked" />
|
||||
<label for="role-@index" class="form-check-label">@item.Name</label>
|
||||
<label for="role-@index" class="form-check-label">@item.Role.Name</label>
|
||||
</div>
|
||||
}
|
||||
</Body>
|
||||
@ -29,37 +29,34 @@
|
||||
|
||||
private List<RoleItem> items = new();
|
||||
|
||||
protected override async Task BeforeShown(IdentityUser user) {
|
||||
var userRoles = await GetUserRoles(user);
|
||||
this.items = RoleManager.Roles
|
||||
.Select(static role => role.Name)
|
||||
.AsEnumerable()
|
||||
.WhereNotNull()
|
||||
.Select(role => new RoleItem(role, userRoles.Contains(role)))
|
||||
.ToList();
|
||||
protected override async Task BeforeShown(UserEntity user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var allRoles = await RoleManager.GetAll();
|
||||
this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList();
|
||||
}
|
||||
|
||||
protected override async Task DoEdit(IdentityUser user) {
|
||||
var userRoles = await GetUserRoles(user);
|
||||
protected override async Task DoEdit(UserEntity user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var addedToRoles = new List<string>();
|
||||
var removedFromRoles = new List<string>();
|
||||
var errors = new List<string>();
|
||||
|
||||
foreach (var item in items) {
|
||||
var shouldHaveRole = item.Checked;
|
||||
var roleName = item.Name;
|
||||
|
||||
if (shouldHaveRole == userRoles.Contains(roleName)) {
|
||||
if (shouldHaveRole == userRoles.Contains(item.Role.RoleGuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = shouldHaveRole ? await UserManager.AddToRoleAsync(user, roleName) : await UserManager.RemoveFromRoleAsync(user, roleName);
|
||||
if (result.Succeeded) {
|
||||
var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles;
|
||||
modifiedList.Add(roleName);
|
||||
bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role);
|
||||
if (success) {
|
||||
var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles;
|
||||
modifiedList.Add(item.Role.Name);
|
||||
}
|
||||
else if (shouldHaveRole) {
|
||||
errors.Add("Could not add role " + item.Role.Name + " to user.");
|
||||
}
|
||||
else {
|
||||
errors.AddRange(result.Errors.Select(static error => error.Description));
|
||||
errors.Add("Could not remove role " + item.Role.Name + " from user.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,16 +69,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ImmutableHashSet<string>> GetUserRoles(IdentityUser user) {
|
||||
return (await UserManager.GetRolesAsync(user)).ToImmutableHashSet();
|
||||
}
|
||||
|
||||
private sealed class RoleItem {
|
||||
public string Name { get; }
|
||||
public RoleEntity Role { get; }
|
||||
public bool Checked { get; set; }
|
||||
|
||||
public RoleItem(string name, bool @checked) {
|
||||
this.Name = name;
|
||||
public RoleItem(RoleEntity role, bool @checked) {
|
||||
this.Role = role;
|
||||
this.Checked = @checked;
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using Phantom.Server.Services.Audit;
|
||||
using Phantom.Server.Services.Events;
|
||||
using Phantom.Server.Services.Instances;
|
||||
using Phantom.Server.Services.Rpc;
|
||||
using Phantom.Server.Services.Users;
|
||||
using Phantom.Utils.Tasks;
|
||||
using WebLauncher = Phantom.Server.Web.Launcher;
|
||||
|
||||
@ -36,7 +35,6 @@ sealed class WebConfigurator : WebLauncher.IConfigurator {
|
||||
services.AddSingleton<MinecraftVersions>();
|
||||
services.AddSingleton<MessageToServerListenerFactory>();
|
||||
|
||||
services.AddScoped<IdentityLookup>();
|
||||
services.AddScoped<AuditLog>();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Phantom.Utils.Collections;
|
||||
|
||||
@ -11,4 +12,24 @@ public static class EnumerableExtensions {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ImmutableArray<TSource>> ToImmutableArrayAsync<TSource>(this IAsyncEnumerable<TSource> source, CancellationToken cancellationToken = default) {
|
||||
var builder = ImmutableArray.CreateBuilder<TSource>();
|
||||
|
||||
await foreach (var element in source.WithCancellation(cancellationToken)) {
|
||||
builder.Add(element);
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
public static async Task<ImmutableHashSet<TSource>> ToImmutableSetAsync<TSource>(this IAsyncEnumerable<TSource> source, CancellationToken cancellationToken = default) {
|
||||
var builder = ImmutableHashSet.CreateBuilder<TSource>();
|
||||
|
||||
await foreach (var element in source.WithCancellation(cancellationToken)) {
|
||||
builder.Add(element);
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
37
Utils/Phantom.Utils/Tasks/Result.cs
Normal file
37
Utils/Phantom.Utils/Tasks/Result.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace Phantom.Utils.Tasks;
|
||||
|
||||
public abstract record Result<TValue, TError> {
|
||||
private Result() {}
|
||||
|
||||
public sealed record Ok(TValue Value) : Result<TValue, TError>;
|
||||
|
||||
public sealed record Fail(TError Error) : Result<TValue, TError>;
|
||||
}
|
||||
|
||||
public abstract record Result<TError> {
|
||||
private Result() {}
|
||||
|
||||
public sealed record Ok : Result<TError> {
|
||||
internal static Ok Instance { get; } = new ();
|
||||
}
|
||||
|
||||
public sealed record Fail(TError Error) : Result<TError>;
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
public static Result<TError> Ok<TError>() {
|
||||
return Result<TError>.Ok.Instance;
|
||||
}
|
||||
|
||||
public static Result<TError> Fail<TError>(TError error) {
|
||||
return new Result<TError>.Fail(error);
|
||||
}
|
||||
|
||||
public static Result<TValue, TError> Ok<TValue, TError>(TValue value) {
|
||||
return new Result<TValue, TError>.Ok(value);
|
||||
}
|
||||
|
||||
public static Result<TValue, TError> Fail<TValue, TError>(TError error) {
|
||||
return new Result<TValue, TError>.Fail(error);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user