Add 'backend/' from commit '040cfd7a75423d4e6136e58a67b40579af4ee966'

git-subtree-dir: backend
git-subtree-mainline: ab911955ed
git-subtree-split: 040cfd7a75
This commit is contained in:
2025-01-15 15:24:30 -05:00
179 changed files with 14349 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace Hutopy.Web.Features.Users.Data
{
public class IdentityDbContext(
DbContextOptions<IdentityDbContext> options)
: IdentityDbContext<IdentityUser, IdentityRole, Guid>(options)
{
public const string SchemaName = "Identity";
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema(SchemaName);
}
}
}

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Web.Features.Users.Data;
public static class InitializerExtensions
{
public static async Task InitialiseIdentityDatabaseAsync(this WebApplication app)
{
using var scope = app.Services.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<IdentityDbContextInitializer>();
await initializer.InitialiseAsync();
await initializer.SeedAsync();
}
}
public class IdentityDbContextInitializer(
ILogger<IdentityDbContextInitializer> logger,
IdentityDbContext context,
RoleManager<IdentityRole> roleManager)
{
public async Task InitialiseAsync()
{
try
{
await context.Database.MigrateAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while initialising the database.");
throw;
}
}
public async Task SeedAsync()
{
try
{
await TrySeedAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while seeding the database.");
throw;
}
}
private async Task TrySeedAsync()
{
var administratorRole = new IdentityRole(KnownRoles.Administrator);
if (roleManager.Roles.All(r => r.Name != administratorRole.Name))
{
await roleManager.CreateAsync(administratorRole);
}
var roleCreator = new IdentityRole(KnownRoles.Creator);
if (roleManager.Roles.All(r => r.Name != roleCreator.Name))
{
await roleManager.CreateAsync(roleCreator);
}
}
}

View File

@@ -0,0 +1,304 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Users.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Users.Data.Migrations
{
[DbContext(typeof(IdentityDbContext))]
[Migration("20241020183421_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Identity")
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Features.Users.IdentityRole", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
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("AspNetRoles", "Identity");
});
modelBuilder.Entity("Hutopy.Web.Features.Users.IdentityUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("Address")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Alias")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime?>("BirthDate")
.HasColumnType("timestamp with time zone");
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<string>("Firstname")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("GoogleId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Lastname")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
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>("PortraitUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
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("AspNetUsers", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", 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<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", 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<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
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("AspNetUserTokens", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,260 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Users.Data.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "Identity");
migrationBuilder.CreateTable(
name: "AspNetRoles",
schema: "Identity",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", 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_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
schema: "Identity",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Alias = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
Firstname = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
Lastname = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
BirthDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Address = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
PortraitUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
GoogleId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
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_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
schema: "Identity",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalSchema: "Identity",
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
schema: "Identity",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalSchema: "Identity",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
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<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalSchema: "Identity",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
schema: "Identity",
columns: table => new
{
UserId = table.Column<Guid>(type: "uuid", nullable: false),
RoleId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalSchema: "Identity",
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalSchema: "Identity",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
schema: "Identity",
columns: table => new
{
UserId = table.Column<Guid>(type: "uuid", 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_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalSchema: "Identity",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
schema: "Identity",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
schema: "Identity",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
schema: "Identity",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
schema: "Identity",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
schema: "Identity",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
schema: "Identity",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
schema: "Identity",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims",
schema: "Identity");
migrationBuilder.DropTable(
name: "AspNetUserClaims",
schema: "Identity");
migrationBuilder.DropTable(
name: "AspNetUserLogins",
schema: "Identity");
migrationBuilder.DropTable(
name: "AspNetUserRoles",
schema: "Identity");
migrationBuilder.DropTable(
name: "AspNetUserTokens",
schema: "Identity");
migrationBuilder.DropTable(
name: "AspNetRoles",
schema: "Identity");
migrationBuilder.DropTable(
name: "AspNetUsers",
schema: "Identity");
}
}
}

View File

@@ -0,0 +1,301 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Users.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Users.Data.Migrations
{
[DbContext(typeof(IdentityDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Identity")
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Features.Users.IdentityRole", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
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("AspNetRoles", "Identity");
});
modelBuilder.Entity("Hutopy.Web.Features.Users.IdentityUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("Address")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Alias")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime?>("BirthDate")
.HasColumnType("timestamp with time zone");
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<string>("Firstname")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("GoogleId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Lastname")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
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>("PortraitUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
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("AspNetUsers", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", 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<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", 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<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
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("AspNetUserTokens", "Identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,43 @@
using Hutopy.Web.Common.BlobStorage;
using Hutopy.Web.Features.Messages.Data;
using Hutopy.Web.Features.Users.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Web.Features.Users;
public static class DependencyInjection
{
public static WebApplicationBuilder AddIdentityModule(
this WebApplicationBuilder builder,
Action<DbContextOptionsBuilder>? configureAction = null)
{
builder.Services.AddDbContext<MessagingDbContext>(configureAction);
builder.Services.AddScoped<MessagingDbContextInitializer>();
builder.Services.AddDbContext<IdentityDbContext>(configureAction);
builder.Services.AddScoped<IdentityDbContextInitializer>();
builder.Services.AddAuthentication()
.AddBearerToken(IdentityConstants.BearerScheme);
builder.Services.AddAuthorizationBuilder();
builder.Services
.AddIdentityCore<IdentityUser>()
.AddUserManager<IdentityUserManager>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>()
.AddApiEndpoints()
.AddSignInManager<SignInManager<IdentityUser>>()
.AddDefaultTokenProviders();
// Singleton services
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<AzureBlobStorage>();
// Scoped services
builder.Services.AddScoped<IdentityService>();
return builder;
}
}

View File

@@ -0,0 +1,41 @@
using Hutopy.Web.Common.Security;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public record ChangeAddressRequest(
string? Address);
[PublicAPI]
public class ChangeAddressHandler(
IdentityUserManager userManager)
: Endpoint<ChangeAddressRequest>
{
public override void Configure()
{
Post("/api/users/address");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
ChangeAddressRequest request,
CancellationToken ct)
{
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
user.Address = request.Address;
var result = await userManager.UpdateAsync(user);
if (result.Succeeded)
await SendOkAsync(ct);
else
await SendUnauthorizedAsync(ct);
}
}

View File

@@ -0,0 +1,41 @@
using Hutopy.Web.Common.Security;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public record ChangeAliasRequest(
string? Alias);
[PublicAPI]
public class ChangeAliasHandler(
IdentityUserManager userManager)
: Endpoint<ChangeAliasRequest>
{
public override void Configure()
{
Post("/api/users/alias");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
ChangeAliasRequest request,
CancellationToken ct)
{
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
user.Alias = request.Alias;
var result = await userManager.UpdateAsync(user);
if (result.Succeeded)
await SendOkAsync(ct);
else
await SendUnauthorizedAsync(ct);
}
}

View File

@@ -0,0 +1,41 @@
using Hutopy.Web.Common.Security;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public record ChangeBirthDateRequest(
DateTime BirthDate);
[PublicAPI]
public class ChangeBirthDateHandler(
IdentityUserManager userManager)
: Endpoint<ChangeBirthDateRequest>
{
public override void Configure()
{
Post("/api/users/birthdate");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
ChangeBirthDateRequest request,
CancellationToken ct)
{
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
user.BirthDate = request.BirthDate;
var result = await userManager.UpdateAsync(user);
if (result.Succeeded)
await SendOkAsync(ct);
else
await SendUnauthorizedAsync(ct);
}
}

View File

@@ -0,0 +1,42 @@
using Hutopy.Web.Common.Security;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public record ChangeEmailRequest(
string? Email);
[PublicAPI]
public class ChangeEmailHandler(
IdentityUserManager userManager)
: Endpoint<ChangeEmailRequest>
{
public override void Configure()
{
Post("/api/users/email");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
ChangeEmailRequest request,
CancellationToken ct)
{
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
user.Email = request.Email;
// TODO: check to see if identity resets the `email confirmed` flag - @jonathan
var result = await userManager.UpdateAsync(user);
if (result.Succeeded)
await SendOkAsync(ct);
else
await SendUnauthorizedAsync(ct);
}
}

View File

@@ -0,0 +1,43 @@
using Hutopy.Web.Common.Security;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public record ChangeFullnameRequest(
string? Firstname,
string? Lastname);
[PublicAPI]
public class ChangeFullnameHandler(
IdentityUserManager userManager)
: Endpoint<ChangeFullnameRequest>
{
public override void Configure()
{
Post("/api/users/fullname");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
ChangeFullnameRequest request,
CancellationToken ct)
{
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
user.Firstname = request.Firstname;
user.Lastname = request.Lastname;
var result = await userManager.UpdateAsync(user);
if (result.Succeeded)
await SendOkAsync(ct);
else
await SendUnauthorizedAsync(ct);
}
}

View File

@@ -0,0 +1,42 @@
using Hutopy.Web.Common.Security;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public record ChangePhoneRequest(
string? PhoneNumber);
[PublicAPI]
public class ChangePhoneHandler(
IdentityUserManager userManager)
: Endpoint<ChangePhoneRequest>
{
public override void Configure()
{
Post("/api/users/phone");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
ChangePhoneRequest request,
CancellationToken ct)
{
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
user.PhoneNumber = request.PhoneNumber;
// TODO: check to see if identity resets the `phone confirmed` flag - @jonathan
var result = await userManager.UpdateAsync(user);
if (result.Succeeded)
await SendOkAsync(ct);
else
await SendUnauthorizedAsync(ct);
}
}

View File

@@ -0,0 +1,72 @@
using Hutopy.Web.Common.BlobStorage;
using Hutopy.Web.Common.Security;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public record ChangePortraitRequest(
IFormFile File);
[PublicAPI]
public record ChangePortraitResponse(
string BlobUrl);
[PublicAPI]
public sealed class ChangePortraitRequestValidator : Validator<ChangePortraitRequest>
{
public ChangePortraitRequestValidator()
{
RuleFor(x => x.File)
.NotNull()
.NotEmpty();
}
}
[PublicAPI]
public class ChangePortraitHandler(
IdentityUserManager userManager,
AzureBlobStorage blobStorage)
: Endpoint<ChangePortraitRequest, ChangePortraitResponse>
{
public override void Configure()
{
Post("/api/users/portrait");
Options(o => o.WithTags("Users"));
AllowFileUploads();
}
public override async Task HandleAsync(
ChangePortraitRequest request,
CancellationToken ct)
{
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
var blobUrl = await blobStorage.UploadFileAsync(
ContainerNames.Users,
$"{user.Id}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
request.File.OpenReadStream(),
request.File.ContentType,
ct);
user.PortraitUrl = blobUrl;
var result = await userManager.UpdateAsync(user);
if (result.Succeeded)
{
await SendOkAsync(
new ChangePortraitResponse(blobUrl),
ct);
}
else
{
await SendUnauthorizedAsync(ct);
}
}
}

View File

@@ -0,0 +1,56 @@
using Hutopy.Web.Features.Users.Handlers.Models;
using Hutopy.Web.Features.Memberships.Data;
using Hutopy.Web.Features.Memberships.Infrastructure;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public class GetCurrentUserQueryHandler(
IdentityService identityService,
MembershipDbContext membershipDbContext)
: EndpointWithoutRequest<UserDto>
{
public override void Configure()
{
Get("/api/users/profile");
Options(o => o.WithTags("Memberships"));
}
public override async Task HandleAsync(
CancellationToken cancellationToken)
{
var userModel = await identityService.GetCurrentUserAsync();
if (userModel is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var roles = await identityService.GetCurrentUserRolesAsync();
var stripeId = await membershipDbContext
.Creators
.Where(c => c.Id == userModel.Id)
.Select(c => c.StripeAccountId)
.FirstOrDefaultAsync(cancellationToken);
await SendOkAsync(
new UserDto
{
Id = userModel.Id,
Alias = userModel.Alias,
PortraitUrl = userModel.PortraitUrl,
Firstname = userModel.Firstname,
Lastname = userModel.Lastname,
Username = userModel.Username,
PhoneNumber = userModel.PhoneNumber,
Email = userModel.Email,
BirthDate = userModel.BirthDate,
Address = userModel.Address,
UserRoles = roles,
StripeId = stripeId ?? string.Empty
},
cancellationToken);
}
}

View File

@@ -0,0 +1,30 @@
using Hutopy.Web.Common.BlobStorage;
namespace Hutopy.Web.Features.Users.Handlers;
[PublicAPI]
public class GetCurrentUserPortraitHandler(
IdentityService identityService,
AzureBlobStorage blobStorage
)
: EndpointWithoutRequest<Stream>
{
public override void Configure()
{
Get("/api/users/portrait");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
CancellationToken cancellationToken)
{
var identityUser = await identityService.GetCurrentUserAsync();
var stream = await blobStorage.DownloadFileAsync(
ContainerNames.Users,
$"{identityUser.Id.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
cancellationToken);
await SendOkAsync(stream, cancellationToken);
}
}

View File

@@ -0,0 +1,139 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Hutopy.Web.Common.Security;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace Hutopy.Web.Features.Users.Handlers;
class GoogleToken
{
[JsonPropertyName("access_token")] public required string AccessToken { get; init; }
[JsonPropertyName("token_type")] public required string TokenType { get; init; }
[JsonPropertyName("expires_in")] public required int ExpiresIn { get; init; }
[JsonPropertyName("scope")] public required string Scope { get; init; }
[JsonPropertyName("authuser")] public required string AuthUser { get; init; }
[JsonPropertyName("prompt")] public required string Prompt { get; init; }
}
public class GoogleUserInfo
{
[JsonPropertyName("id")] public required string Id { get; init; }
[JsonPropertyName("email")] public required string Email { get; init; }
[JsonPropertyName("verified_email")] public required bool VerifiedEmail { get; init; }
[JsonPropertyName("name")] public required string Name { get; init; }
[JsonPropertyName("given_name")] public required string GivenName { get; init; }
[JsonPropertyName("family_name")] public string FamilyName { get; init; } = string.Empty;
[JsonPropertyName("picture")] public required string Picture { get; init; }
}
[PublicAPI]
public record LoginWithGoogleRequest(
string Token);
[PublicAPI]
public record LoginWithGoogleResponse(
string AccessToken,
string RefreshToken);
[PublicAPI]
public class LoginWithGoogleHandler(
IHttpClientFactory httpClientFactory,
IdentityUserManager userManager,
SignInManager<IdentityUser> signInManager,
IOptionsSnapshot<JwtOptions> jwtOptions)
: Endpoint<LoginWithGoogleRequest, LoginWithGoogleResponse>
{
public override void Configure()
{
AllowAnonymous();
Post("/api/users/login-with-google");
Options(o => o.WithTags("Users"));
}
public override async Task HandleAsync(
LoginWithGoogleRequest request,
CancellationToken ct)
{
var googleToken = JsonSerializer.Deserialize<GoogleToken>(request.Token)!;
// Verify the token with Google
using var httpClient = httpClientFactory.CreateClient();
using var response = await httpClient.GetAsync(
$"https://www.googleapis.com/oauth2/v1/userinfo?access_token={googleToken.AccessToken}",
ct);
if (!response.IsSuccessStatusCode)
{
await SendStringAsync(
"The token is not valid",
400,
cancellation: ct);
return;
}
// Extract the user info (email, name, etc.).
var content = await response.Content.ReadAsStringAsync(ct);
var userInfo = JsonSerializer.Deserialize<GoogleUserInfo>(content);
if (userInfo is null
|| !userInfo.VerifiedEmail
|| string.IsNullOrEmpty(userInfo.Email))
{
await SendStringAsync(
"The token does not contain an email",
400,
cancellation: ct);
return;
}
// Check if user exists or create a new one
var user = await userManager.FindByEmailAsync(userInfo.Email);
if (user is null)
{
var generatedPassword = PasswordGenerator.GeneratePassword(10, 12);
var generatedUser = new IdentityUser
{
UserName = userInfo.Email,
Email = userInfo.Email,
Firstname = userInfo.GivenName,
Lastname = userInfo.FamilyName,
Alias = userInfo.Name,
PortraitUrl = userInfo.Picture,
GoogleId = userInfo.Id,
};
var result = await userManager.CreateAsync(
generatedUser,
generatedPassword);
if (!result.Succeeded)
{
await SendStringAsync(
result.Errors.First().Description,
400,
cancellation: ct);
return;
}
user = generatedUser;
}
await signInManager.SignInAsync(user, isPersistent: false);
var accessToken = JwtTokenHelper.GenerateJwtToken(
expiresIn: jwtOptions.Value.Lifetime,
issuer: jwtOptions.Value.Issuer,
audience: jwtOptions.Value.Audience,
key: jwtOptions.Value.Key,
userId: user.Id.ToString(),
email: user.Email,
alias: user.Alias,
firstname: user.Firstname,
lastname: user.Lastname,
portraitUrl: user.PortraitUrl);
await SendOkAsync(
new LoginWithGoogleResponse(accessToken, string.Empty),
cancellation: ct);
}
}

View File

@@ -0,0 +1,17 @@
namespace Hutopy.Web.Features.Users.Handlers.Models;
public class UserDto
{
public Guid Id { get; init; }
public IList<string> UserRoles { get; init; } = [];
public string Username { get; init; } = null!;
public string? Alias { get; init; }
public string? PortraitUrl { get; init; }
public string? Firstname { get; init; }
public string? Lastname { get; init; }
public string? Email { get; init; }
public string? PhoneNumber { get; init; }
public DateTime? BirthDate { get; init; }
public string? Address { get; init; }
public string? StripeId { get; init; }
}

View File

@@ -0,0 +1,14 @@
using Hutopy.Web.Features.Users.Models;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Web.Features.Users;
public static class IdentityResultExtensions
{
public static Result ToApplicationResult(this IdentityResult result)
{
return result.Succeeded
? Result.Success()
: Result.Failure(result.Errors.Select(e => e.Description));
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Web.Features.Users;
public class IdentityRole : IdentityRole<Guid>
{
public IdentityRole() { }
public IdentityRole(string roleName) : base(roleName) { }
}

View File

@@ -0,0 +1,94 @@
using System.Security.Claims;
using Hutopy.Web.Features.Users.Models;
namespace Hutopy.Web.Features.Users;
public class IdentityService(
IdentityUserManager userManager,
IHttpContextAccessor contextAccessor
)
{
public async Task<Result<Guid>> CreateUserAsync(string email, string userName, string firstName, string lastName,
string password)
{
var applicationUser = new IdentityUser
{
UserName = userName, Email = email, Firstname = firstName, Lastname = lastName
};
var response = await userManager.CreateAsync(applicationUser, password);
if (!response.Succeeded)
{
throw new BadHttpRequestException(response.Errors.First().Description);
}
var result = new Result<Guid>(applicationUser.Id, response.Succeeded, response.ToApplicationResult().Errors);
return result;
}
private static UserModel BuildModelFrom(IdentityUser response)
{
var userModel = new UserModel
{
Id = response.Id,
Username = response.UserName ?? string.Empty,
PhoneNumber = response.PhoneNumber ?? string.Empty,
Email = response.Email ?? string.Empty,
PortraitUrl = response.PortraitUrl,
Alias = response.Alias,
Firstname = response.Firstname,
Lastname = response.Lastname,
BirthDate = response.BirthDate,
Address = response.Address,
};
return userModel;
}
public async Task<UserModel?> FindUserByIdAsync(string id)
{
var user = await userManager.FindByIdAsync(id);
if (user == null) return null;
var userModel = BuildModelFrom(user);
return userModel;
}
public async Task<UserModel?> FindUserByEmailAsync(string email)
{
var response = await userManager.FindByEmailAsync(email);
if (response == null) return null;
return BuildModelFrom(response);
}
public async Task<UserModel?> GetCurrentUserAsync()
{
var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(currentUserId))
{
return null;
}
return await FindUserByIdAsync(currentUserId);
}
public async Task<IList<string>> GetCurrentUserRolesAsync()
{
var currentUserModel = await GetCurrentUserAsync();
var currentUser = await userManager.FindByIdAsync(currentUserModel.Id.ToString());
if (currentUser is null) return [];
var userRoles = await userManager.GetRolesAsync(currentUser);
return userRoles;
}
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Web.Features.Users;
public class IdentityUser : IdentityUser<Guid>
{
[MaxLength(255)] public string? Alias { get; set; }
[MaxLength(255)] public string? Firstname { get; set; }
[MaxLength(255)] public string? Lastname { get; set; }
public DateTime? BirthDate { get; set; }
[MaxLength(255)] public string? Address { get; set; }
[MaxLength(255)] public string? PortraitUrl { get; set; }
[MaxLength(255)] public string? GoogleId { get; set; }
}

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace Hutopy.Web.Features.Users;
public sealed class IdentityUserManager(
IUserStore<IdentityUser> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<IdentityUser>> logger)
: UserManager<IdentityUser>(
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger)
{
}

View File

@@ -0,0 +1,11 @@
namespace Hutopy.Web.Features.Users;
public record JwtOptions
{
public const string SectionName = "Authentication:Jwt";
public required TimeSpan Lifetime { get; init; }
public required string Issuer { get; init; }
public required string Audience { get; init; }
public required string Key { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace Hutopy.Web.Features.Users;
public static class KnownRoles
{
public const string Administrator = nameof(Administrator);
public const string Creator = nameof(Creator);
}

View File

@@ -0,0 +1,49 @@
namespace Hutopy.Web.Features.Users.Models;
public class Result(
bool succeeded,
IEnumerable<string> errors)
{
public bool Succeeded { get; init; } = succeeded;
public string[] Errors { get; init; } = errors.ToArray();
public static Result Success()
{
return new Result(true, Array.Empty<string>());
}
public static Result Failure(IEnumerable<string> errors)
{
return new Result(false, errors);
}
}
public class Result<T>(
T? value,
bool succeeded,
IEnumerable<string> errors)
{
public bool Succeeded { get; init; } = succeeded;
public string[] Errors { get; init; } = errors.ToArray();
public T? Value { get; set; } = value;
public T GetValueOrDefault()
{
return Value ?? default(T)!;
}
public string GetErrorsAsString()
{
return Errors.Length == 0 ? string.Empty : string.Join(", ", Errors);
}
public static Result<T> Success(T value)
{
return new Result<T>(value, true, Array.Empty<string>());
}
public static Result<T> Failure(T value, IEnumerable<string> errors)
{
return new Result<T>(value, false, errors);
}
}

View File

@@ -0,0 +1,7 @@
namespace Hutopy.Web.Features.Users.Models;
public class RoleModel
{
public Guid Id { get; set; }
public string? Name { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace Hutopy.Web.Features.Users.Models;
public class UserModel
{
public Guid Id { get; set; }
public string Username { get; init; } = null!;
public string? Alias { get; init; }
public string? PortraitUrl { get; init; }
public string? Firstname { get; init; }
public string? Lastname { get; init; }
public string? Email { get; init; }
public string? PhoneNumber { get; init; }
public DateTime? BirthDate { get; init; }
public string? Address { get; init; }
}