mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-01-05 13:42:46 +01:00
Add administrator account creation and user login
This commit is contained in:
parent
adea2021ba
commit
adf0dd6853
Server
Phantom.Server.Database.Postgres/Migrations
Phantom.Server.Database
Phantom.Server.Services
Phantom.Server.Web.Components
Forms
Utils
Phantom.Server.Web
App.razor
Authentication
BlazorIdentityMiddleware.csPhantomLoginManager.csPhantomLoginStore.csRevalidatingIdentityAuthenticationStateProvider.cs
Configuration.csLauncher.csLayout
Pages
_Imports.razorPhantom.Server
342
Server/Phantom.Server.Database.Postgres/Migrations/20221008163849_Identity.Designer.cs
generated
Normal file
342
Server/Phantom.Server.Database.Postgres/Migrations/20221008163849_Identity.Designer.cs
generated
Normal file
@ -0,0 +1,342 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
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("20221008163849_Identity")]
|
||||
partial class Identity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.0-rc.1.22426.7")
|
||||
.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")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("MaxInstances")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<ushort>("MaxMemory")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("AgentGuid");
|
||||
|
||||
b.ToTable("Agents", "agents");
|
||||
});
|
||||
|
||||
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<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("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
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();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Phantom.Server.Database.Postgres.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Identity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "identity");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Roles",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", 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),
|
||||
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
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),
|
||||
RoleId = table.Column<string>(type: "text", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
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: "UserClaims",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
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: "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_RoleClaims_RoleId",
|
||||
schema: "identity",
|
||||
table: "RoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
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_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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "RoleClaims",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserClaims",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserLogins",
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,202 @@ namespace Phantom.Server.Database.Postgres.Migrations
|
||||
|
||||
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")
|
||||
@ -86,6 +282,57 @@ namespace Phantom.Server.Database.Postgres.Migrations
|
||||
|
||||
b.ToTable("Instances", "agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
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();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
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;
|
||||
@ -10,7 +12,7 @@ using Phantom.Server.Database.Factories;
|
||||
namespace Phantom.Server.Database;
|
||||
|
||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
||||
public class ApplicationDbContext : DbContext {
|
||||
public class ApplicationDbContext : IdentityDbContext {
|
||||
public DbSet<AgentEntity> Agents { get; set; } = null!;
|
||||
public DbSet<InstanceEntity> Instances { get; set; } = null!;
|
||||
|
||||
@ -22,6 +24,21 @@ public class ApplicationDbContext : DbContext {
|
||||
InstanceUpsert = new InstanceEntityUpsert(this);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder) {
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
const string IdentitySchema = "identity";
|
||||
|
||||
builder.Entity<IdentityRole>().ToTable("Roles", schema: IdentitySchema);
|
||||
builder.Entity<IdentityRoleClaim<string>>().ToTable("RoleClaims", schema: IdentitySchema);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder builder) {
|
||||
base.ConfigureConventions(builder);
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
namespace Phantom.Server.Services;
|
||||
using Phantom.Utils.Threading;
|
||||
|
||||
namespace Phantom.Server.Services;
|
||||
|
||||
public sealed record ServiceConfiguration(
|
||||
byte[] AdministratorToken,
|
||||
TaskManager TaskManager,
|
||||
CancellationToken CancellationToken
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ namespace Phantom.Server.Web.Components.Forms.Fields;
|
||||
|
||||
public sealed class InputFieldText : InputBase<string?>, ICustomFormField {
|
||||
[Parameter]
|
||||
public FormNumberInputType Type { get; set; }
|
||||
public FormTextInputType Type { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<ChangeEventArgs> OnChange { get; set; }
|
||||
@ -29,7 +29,7 @@ public sealed class InputFieldText : InputBase<string?>, ICustomFormField {
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder) {
|
||||
builder.OpenElement(0, "input");
|
||||
builder.AddMultipleAttributes(1, AdditionalAttributes);
|
||||
builder.AddAttribute(2, "type", "text");
|
||||
builder.AddAttribute(2, "type", Type.GetHtmlInputType());
|
||||
|
||||
if (!string.IsNullOrEmpty(CssClass)) {
|
||||
builder.AddAttribute(3, "class", CssClass);
|
||||
|
@ -1,5 +1,12 @@
|
||||
@if (!string.IsNullOrEmpty(Message)) {
|
||||
<div class="text-danger my-2 w-100">@Message</div>
|
||||
@if (messageLines.Length > 0) {
|
||||
<div class="text-danger my-2 w-100">
|
||||
@for (int i = 0; i < messageLines.Length; i++) {
|
||||
@messageLines[i]
|
||||
if (i < messageLines.Length - 1) {
|
||||
<br />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
@ -7,4 +14,10 @@
|
||||
[Parameter]
|
||||
public string? Message { get; set; }
|
||||
|
||||
private string[] messageLines = Array.Empty<string>();
|
||||
|
||||
protected override void OnParametersSet() {
|
||||
messageLines = Message?.Split('\n') ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<FormLabel Id="@Id" Label="@Label" LabelFragment="@LabelFragment" />
|
||||
<InputFieldText @ref="FormField"
|
||||
Type="@Type"
|
||||
Value="@Value"
|
||||
ValueChanged="@ValueChanged"
|
||||
ValueExpression="@ValueExpression"
|
||||
@ -11,7 +12,10 @@
|
||||
<ValidationMessage For="@ValueExpression" class="invalid-feedback" />
|
||||
|
||||
@code {
|
||||
|
||||
|
||||
[Parameter]
|
||||
public FormTextInputType Type { get; set; } = FormTextInputType.Text;
|
||||
|
||||
protected override ICustomFormField FormField { get; set; } = null!;
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
namespace Phantom.Server.Web.Components.Forms;
|
||||
|
||||
public enum FormTextInputType {
|
||||
Text,
|
||||
Password
|
||||
}
|
||||
|
||||
static class FormTextInputTypes {
|
||||
public static string GetHtmlInputType(this FormTextInputType type) {
|
||||
return type switch {
|
||||
FormTextInputType.Text => "text",
|
||||
FormTextInputType.Password => "password",
|
||||
_ => throw new InvalidOperationException($"Unsupported input type {type}")
|
||||
};
|
||||
}
|
||||
}
|
15
Server/Phantom.Server.Web.Components/Utils/Query.cs
Normal file
15
Server/Phantom.Server.Web.Components/Utils/Query.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Web;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Phantom.Server.Web.Components.Utils;
|
||||
|
||||
public static class Query {
|
||||
public static bool GetParameter(NavigationManager navigationManager, string key, [MaybeNullWhen(false)] out string value) {
|
||||
var uri = navigationManager.ToAbsoluteUri(navigationManager.Uri);
|
||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
value = query.Get(key);
|
||||
return value != null;
|
||||
}
|
||||
}
|
@ -1,7 +1,16 @@
|
||||
<CascadingAuthenticationState>
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
@{
|
||||
var returnUrl = Nav.ToBaseRelativePath(Nav.Uri);
|
||||
Nav.NavigateTo("/login" + QueryString.Create("return", returnUrl), forceLoad: true);
|
||||
}
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
|
@ -0,0 +1,26 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Phantom.Server.Web.Authentication;
|
||||
|
||||
sealed class BlazorIdentityMiddleware {
|
||||
private readonly RequestDelegate next;
|
||||
|
||||
public BlazorIdentityMiddleware(RequestDelegate next) {
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
public async Task InvokeAsync(HttpContext context, PhantomLoginManager loginManager) {
|
||||
var path = context.Request.Path;
|
||||
if (path == "/login" && context.Request.Query.TryGetValue("token", out var tokens) && tokens[0] is {} token && await loginManager.ProcessTokenAndGetReturnUrl(token) is {} returnUrl) {
|
||||
context.Response.Redirect(returnUrl);
|
||||
}
|
||||
else if (path == "/logout") {
|
||||
await loginManager.SignOut();
|
||||
context.Response.Redirect("/");
|
||||
}
|
||||
else {
|
||||
await next.Invoke(context);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Server.Web.Authentication;
|
||||
|
||||
sealed class PhantomLoginManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginManager>();
|
||||
|
||||
private readonly PhantomLoginStore loginStore;
|
||||
private readonly NavigationManager navigationManager;
|
||||
private readonly UserManager<IdentityUser> userManager;
|
||||
private readonly SignInManager<IdentityUser> signInManager;
|
||||
|
||||
public PhantomLoginManager(PhantomLoginStore loginStore, NavigationManager navigationManager, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager) {
|
||||
this.loginStore = loginStore;
|
||||
this.navigationManager = navigationManager;
|
||||
this.userManager = userManager;
|
||||
this.signInManager = signInManager;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> SignIn(string username, string password, string returnUrl) {
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
if (user == null) {
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
var result = await signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: true);
|
||||
if (result == SignInResult.Success) {
|
||||
Logger.Verbose("Created login token for {Username}.", username);
|
||||
|
||||
string token = TokenGenerator.Create(60);
|
||||
loginStore.Add(token, user, password, returnUrl);
|
||||
navigationManager.NavigateTo("/login" + QueryString.Create("token", token), forceLoad: true);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task SignOut() {
|
||||
await signInManager.SignOutAsync();
|
||||
}
|
||||
|
||||
public async Task<string?> ProcessTokenAndGetReturnUrl(string token) {
|
||||
var entry = loginStore.Pop(token);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await signInManager.PasswordSignInAsync(entry.User, entry.Password, lockoutOnFailure: false, isPersistent: true);
|
||||
if (result == SignInResult.Success) {
|
||||
Logger.Information("Successful login for {Username}.", entry.User.UserName);
|
||||
return entry.ReturnUrl;
|
||||
}
|
||||
else {
|
||||
Logger.Warning("Error logging in {Username}: {Result}.", entry.User.UserName, result);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Server.Services;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Server.Web.Authentication;
|
||||
|
||||
sealed class PhantomLoginStore {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginStore>();
|
||||
private static readonly TimeSpan ExpirationTime = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly ConcurrentDictionary<string, LoginEntry> loginEntries = new ();
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
public PhantomLoginStore(ServiceConfiguration serviceConfiguration) {
|
||||
this.cancellationToken = serviceConfiguration.CancellationToken;
|
||||
serviceConfiguration.TaskManager.Run(RunExpirationLoop);
|
||||
}
|
||||
|
||||
private async Task RunExpirationLoop() {
|
||||
try {
|
||||
while (true) {
|
||||
await Task.Delay(ExpirationTime, cancellationToken);
|
||||
|
||||
foreach (var (token, entry) in loginEntries) {
|
||||
if (entry.IsExpired) {
|
||||
Logger.Verbose("Expired login entry for {Username}.", entry.User.UserName);
|
||||
loginEntries.TryRemove(token, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Logger.Information("Expiration loop stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(string token, IdentityUser user, string password, string returnUrl) {
|
||||
loginEntries[token] = new LoginEntry(user, password, returnUrl, Stopwatch.StartNew());
|
||||
}
|
||||
|
||||
public LoginEntry? Pop(string token) {
|
||||
if (!loginEntries.TryRemove(token, out var entry)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entry.IsExpired) {
|
||||
Logger.Verbose("Expired login entry for {Username}.", entry.User.UserName);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
public sealed record LoginEntry(IdentityUser User, string Password, string ReturnUrl, Stopwatch AddedTime) {
|
||||
public bool IsExpired => AddedTime.Elapsed >= ExpirationTime;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
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.Authentication;
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,5 +3,5 @@
|
||||
namespace Phantom.Server.Web;
|
||||
|
||||
public sealed record Configuration(ILogger Logger, string Host, ushort Port, CancellationToken CancellationToken) {
|
||||
internal string HttpUrl => "http://" + Host + ":" + Port;
|
||||
public string HttpUrl => "http://" + Host + ":" + Port;
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Web.Authentication;
|
||||
using Phantom.Server.Web.Components.Utils;
|
||||
using Serilog;
|
||||
|
||||
@ -27,9 +31,17 @@ public static class Launcher {
|
||||
builder.Services.AddDbContextPool<ApplicationDbContext>(dbOptionsBuilder, poolSize: 64);
|
||||
builder.Services.AddSingleton<DatabaseProvider>();
|
||||
|
||||
builder.Services.AddSingleton<PhantomLoginStore>();
|
||||
builder.Services.AddScoped<PhantomLoginManager>();
|
||||
builder.Services.AddIdentity<IdentityUser, IdentityRole>(ConfigureIdentity).AddEntityFrameworkStores<ApplicationDbContext>();
|
||||
builder.Services.ConfigureApplicationCookie(ConfigureIdentityCookie);
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddRazorPages(static options => options.RootDirectory = "/Layout");
|
||||
builder.Services.AddServerSideBlazor();
|
||||
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
|
||||
|
||||
var application = builder.Build();
|
||||
|
||||
await MigrateDatabase(config, application.Services.GetRequiredService<DatabaseProvider>());
|
||||
@ -51,6 +63,7 @@ public static class Launcher {
|
||||
application.UseRouting();
|
||||
application.UseAuthentication();
|
||||
application.UseAuthorization();
|
||||
application.UseMiddleware<BlazorIdentityMiddleware>();
|
||||
|
||||
application.MapControllers();
|
||||
application.MapBlazorHub();
|
||||
@ -70,6 +83,36 @@ public static class Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
o.Cookie.SameSite = SameSiteMode.Lax;
|
||||
|
||||
o.ExpireTimeSpan = TimeSpan.FromDays(30);
|
||||
o.SlidingExpiration = true;
|
||||
|
||||
o.LoginPath = "/login";
|
||||
o.LogoutPath = "/logout";
|
||||
o.AccessDeniedPath = "/login";
|
||||
}
|
||||
|
||||
private static async Task MigrateDatabase(Configuration config, DatabaseProvider databaseProvider) {
|
||||
var logger = config.Logger;
|
||||
|
||||
@ -89,7 +132,7 @@ public static class Launcher {
|
||||
}
|
||||
|
||||
public interface IConfigurator {
|
||||
void ConfigureServices(IServiceCollection services);
|
||||
void ConfigureServices( IServiceCollection services);
|
||||
Task LoadFromDatabase(IServiceProvider serviceProvider);
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,17 @@
|
||||
|
||||
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
||||
<nav class="flex-column">
|
||||
<NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" />
|
||||
<NavMenuItem Label="Instances" Icon="folder" Href="instances" />
|
||||
<NavMenuItem Label="Agents" Icon="cloud" Href="agents" />
|
||||
<NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" />
|
||||
<AuthorizeView>
|
||||
<NotAuthorized>
|
||||
<NavMenuItem Label="Login" Icon="account-login" Href="login" />
|
||||
</NotAuthorized>
|
||||
<Authorized>
|
||||
<NavMenuItem Label="Instances" Icon="folder" Href="instances" />
|
||||
<NavMenuItem Label="Agents" Icon="cloud" Href="agents" />
|
||||
<NavMenuItem Label="Logout" Icon="account-logout" Href="logout" />
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Phantom.Server.Web.Layout;
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[AllowAnonymous]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel {
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
|
@ -1,5 +1,12 @@
|
||||
@page "/"
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
You are logged in as @context.User.Identity!.Name.
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
78
Server/Phantom.Server.Web/Pages/Login.razor
Normal file
78
Server/Phantom.Server.Web/Pages/Login.razor
Normal file
@ -0,0 +1,78 @@
|
||||
@page "/login"
|
||||
@using Phantom.Server.Web.Authentication
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Server.Web.Components.Utils
|
||||
@attribute [AllowAnonymous]
|
||||
@inject NavigationManager Nav
|
||||
@inject PhantomLoginManager LoginManager
|
||||
|
||||
<h1>Login</h1>
|
||||
|
||||
<EditForm EditContext="form.EditContext" OnSubmit="DoLogin">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div style="max-width: 400px;">
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" @bind-Value="form.Password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButtonSubmit Label="Login" Model="@form.SubmitModel" class="btn btn-primary" />
|
||||
</div>
|
||||
|
||||
<FormSubmitError Message="@form.SubmitModel.SubmitError" />
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
|
||||
private readonly LoginFormModel form = new ();
|
||||
|
||||
private sealed class LoginFormModel : FormModel {
|
||||
[Required]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
protected override void OnInitialized() {
|
||||
if (Query.GetParameter(Nav, "token", out _)) {
|
||||
form.SubmitModel.StopSubmitting("Please login again.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DoLogin(EditContext context) {
|
||||
if (!context.Validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.SubmitModel.StartSubmitting();
|
||||
|
||||
string? returnUrl = Query.GetParameter(Nav, "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.";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
101
Server/Phantom.Server.Web/Pages/Setup.razor
Normal file
101
Server/Phantom.Server.Web/Pages/Setup.razor
Normal file
@ -0,0 +1,101 @@
|
||||
@page "/setup"
|
||||
@using Phantom.Server.Web.Authentication
|
||||
@using Phantom.Server.Services
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Utils.Cryptography
|
||||
@using System.Security.Cryptography
|
||||
@attribute [AllowAnonymous]
|
||||
@inject PhantomLoginManager LoginManager
|
||||
@inject ServiceConfiguration ServiceConfiguration
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
|
||||
<h1>Administrator Setup</h1>
|
||||
|
||||
<EditForm EditContext="form.EditContext" OnSubmit="DoLogin">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div style="max-width: 400px;">
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" @bind-Value="form.Password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="administration-token" Label="Administration Token" Type="FormTextInputType.Password" @bind-Value="form.AdministrationToken" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButtonSubmit Label="Continue" Model="@form.SubmitModel" class="btn btn-primary" />
|
||||
</div>
|
||||
|
||||
<FormSubmitError Message="@form.SubmitModel.SubmitError" />
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
|
||||
private readonly CreateAdministratorAccountFormModel form = new ();
|
||||
|
||||
private sealed class CreateAdministratorAccountFormModel : FormModel {
|
||||
[Required]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string AdministrationToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private async Task DoLogin(EditContext context) {
|
||||
if (!context.Validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.SubmitModel.StartSubmitting();
|
||||
|
||||
if (!IsAdministratorTokenValid()) {
|
||||
form.SubmitModel.StopSubmitting("Invalid administrator token.");
|
||||
return;
|
||||
}
|
||||
|
||||
IdentityResult createUserResult;
|
||||
var existingUser = await UserManager.FindByNameAsync(form.Username);
|
||||
if (existingUser != null) {
|
||||
await UserManager.RemovePasswordAsync(existingUser);
|
||||
createUserResult = await UserManager.AddPasswordAsync(existingUser, form.Password);
|
||||
}
|
||||
else {
|
||||
createUserResult = await UserManager.CreateAsync(new IdentityUser(form.Username), form.Password);
|
||||
}
|
||||
|
||||
if (!createUserResult.Succeeded) {
|
||||
form.SubmitModel.StopSubmitting(string.Join("\n", createUserResult.Errors.Select(static error => error.Description)));
|
||||
return;
|
||||
}
|
||||
|
||||
var signInResult = await LoginManager.SignIn(form.Username, form.Password, "/");
|
||||
if (!signInResult.Succeeded) {
|
||||
form.SubmitModel.StopSubmitting("Error logging in.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAdministratorTokenValid() {
|
||||
byte[] formTokenBytes;
|
||||
try {
|
||||
formTokenBytes = TokenGenerator.GetBytesOrThrow(form.AdministrationToken);
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(formTokenBytes, ServiceConfiguration.AdministratorToken);
|
||||
}
|
||||
}
|
@ -11,3 +11,4 @@
|
||||
@using Phantom.Server.Web.Components.Graphics
|
||||
@using Phantom.Server.Web.Layout
|
||||
@using Phantom.Server.Web.Shared
|
||||
@attribute [Authorize]
|
||||
|
@ -4,7 +4,9 @@ using Phantom.Common.Logging;
|
||||
using Phantom.Server;
|
||||
using Phantom.Server.Database.Postgres;
|
||||
using Phantom.Server.Rpc;
|
||||
using Phantom.Server.Services;
|
||||
using Phantom.Server.Services.Rpc;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Runtime;
|
||||
@ -49,7 +51,12 @@ try {
|
||||
|
||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel server...");
|
||||
|
||||
var webConfigurator = new WebConfigurator(agentToken, cancellationTokenSource.Token);
|
||||
var administratorToken = TokenGenerator.Create(60);
|
||||
PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken);
|
||||
PhantomLogger.Root.Information("For administrator setup, visit: {BaseUrl}/setup", webConfiguration.HttpUrl);
|
||||
|
||||
var serviceConfiguration = new ServiceConfiguration(TokenGenerator.GetBytesOrThrow(administratorToken), taskManager, cancellationTokenSource.Token);
|
||||
var webConfigurator = new WebConfigurator(agentToken, serviceConfiguration);
|
||||
var webApplication = await WebLauncher.CreateApplication(webConfiguration, webConfigurator, options => options.UseNpgsql(sqlConnectionString, static options => {
|
||||
options.CommandTimeout(10).MigrationsAssembly(typeof(ApplicationDbContextDesignFactory).Assembly.FullName);
|
||||
}));
|
||||
|
@ -10,15 +10,15 @@ namespace Phantom.Server;
|
||||
|
||||
sealed class WebConfigurator : WebLauncher.IConfigurator {
|
||||
private readonly AgentAuthToken agentToken;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly ServiceConfiguration serviceConfiguration;
|
||||
|
||||
public WebConfigurator(AgentAuthToken agentToken, CancellationToken cancellationToken) {
|
||||
public WebConfigurator(AgentAuthToken agentToken, ServiceConfiguration serviceConfiguration) {
|
||||
this.agentToken = agentToken;
|
||||
this.cancellationToken = cancellationToken;
|
||||
this.serviceConfiguration = serviceConfiguration;
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services) {
|
||||
services.AddSingleton(new ServiceConfiguration(cancellationToken));
|
||||
services.AddSingleton(serviceConfiguration);
|
||||
services.AddSingleton(agentToken);
|
||||
services.AddSingleton<AgentManager>();
|
||||
services.AddSingleton<AgentJavaRuntimesManager>();
|
||||
|
Loading…
Reference in New Issue
Block a user