1
0
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:
chylex 2022-10-08 15:54:25 +02:00
parent adea2021ba
commit adf0dd6853
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
25 changed files with 1386 additions and 22 deletions

View 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
}
}
}

View File

@ -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");
}
}
}

View File

@ -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
}
}

View File

@ -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);

View File

@ -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
);

View File

@ -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);

View File

@ -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>();
}
}

View File

@ -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!;
}

View File

@ -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}")
};
}
}

View 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;
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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; }

View File

@ -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>

View 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.";
}
}
}

View 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);
}
}

View File

@ -11,3 +11,4 @@
@using Phantom.Server.Web.Components.Graphics
@using Phantom.Server.Web.Layout
@using Phantom.Server.Web.Shared
@attribute [Authorize]

View File

@ -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);
}));

View File

@ -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>();