Add user profile fields and completion prompt
Build and Push Docker Image / build-and-push (push) Successful in 23s
Build and Push Docker Image / build-and-push (push) Successful in 23s
Added `FirstName`, `LastName`, and `Address` fields to `ApplicationUser` for extended user profiles. Updated `Index.razor` to include these fields in the profile form with validation. Introduced `ProfileCompletionService` to check profile completeness and added a prompt in `TopBar.razor` for incomplete profiles. Created migration `20260518213355_AddUserProfileFields` to update the database schema. Updated `RegistryService` to use new fields for user display names. Registered `ProfileCompletionService` and `DbContextFactory` in `Program.cs`. Styled the profile completion banner in `TopBar.razor.css`.
This commit is contained in:
@@ -23,6 +23,21 @@
|
|||||||
<input type="text" value="@username" class="form-control" placeholder="Please choose your username." disabled />
|
<input type="text" value="@username" class="form-control" placeholder="Please choose your username." disabled />
|
||||||
<label for="username" class="form-label">Username</label>
|
<label for="username" class="form-label">Username</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<InputText @bind-Value="Input.FirstName" class="form-control" placeholder="Please enter your first name." />
|
||||||
|
<label class="form-label">First name</label>
|
||||||
|
<ValidationMessage For="() => Input.FirstName" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<InputText @bind-Value="Input.LastName" class="form-control" placeholder="Please enter your last name." />
|
||||||
|
<label class="form-label">Last name</label>
|
||||||
|
<ValidationMessage For="() => Input.LastName" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Address</label>
|
||||||
|
<InputTextArea @bind-Value="Input.Address" class="form-control" rows="3" placeholder="Please enter your address." />
|
||||||
|
<ValidationMessage For="() => Input.Address" class="text-danger" />
|
||||||
|
</div>
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<InputText @bind-Value="Input.PhoneNumber" class="form-control" placeholder="Please enter your phone number." />
|
<InputText @bind-Value="Input.PhoneNumber" class="form-control" placeholder="Please enter your phone number." />
|
||||||
<label for="phone-number" class="form-label">Phone number</label>
|
<label for="phone-number" class="form-label">Phone number</label>
|
||||||
@@ -51,6 +66,9 @@
|
|||||||
phoneNumber = await UserManager.GetPhoneNumberAsync(user);
|
phoneNumber = await UserManager.GetPhoneNumberAsync(user);
|
||||||
|
|
||||||
Input.PhoneNumber ??= phoneNumber;
|
Input.PhoneNumber ??= phoneNumber;
|
||||||
|
Input.FirstName ??= user.FirstName;
|
||||||
|
Input.LastName ??= user.LastName;
|
||||||
|
Input.Address ??= user.Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnValidSubmitAsync()
|
private async Task OnValidSubmitAsync()
|
||||||
@@ -64,12 +82,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.FirstName = string.IsNullOrWhiteSpace(Input.FirstName) ? null : Input.FirstName.Trim();
|
||||||
|
user.LastName = string.IsNullOrWhiteSpace(Input.LastName) ? null : Input.LastName.Trim();
|
||||||
|
user.Address = string.IsNullOrWhiteSpace(Input.Address) ? null : Input.Address.Trim();
|
||||||
|
|
||||||
|
var updateResult = await UserManager.UpdateAsync(user);
|
||||||
|
if (!updateResult.Succeeded)
|
||||||
|
{
|
||||||
|
RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to update profile.", HttpContext);
|
||||||
|
}
|
||||||
|
|
||||||
await SignInManager.RefreshSignInAsync(user);
|
await SignInManager.RefreshSignInAsync(user);
|
||||||
RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext);
|
RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class InputModel
|
private sealed class InputModel
|
||||||
{
|
{
|
||||||
|
[StringLength(100)]
|
||||||
|
[Display(Name = "First name")]
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
|
||||||
|
[StringLength(100)]
|
||||||
|
[Display(Name = "Last name")]
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
|
||||||
|
[StringLength(500)]
|
||||||
|
[Display(Name = "Address")]
|
||||||
|
public string? Address { get; set; }
|
||||||
|
|
||||||
[Phone]
|
[Phone]
|
||||||
[Display(Name = "Phone number")]
|
[Display(Name = "Phone number")]
|
||||||
public string? PhoneNumber { get; set; }
|
public string? PhoneNumber { get; set; }
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
|
@using BirthList.Web.Features.Registries
|
||||||
|
@using BirthList.Web.Services
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
|
@inject RegistryUserContext RegistryUserContext
|
||||||
|
@inject ProfileCompletionService ProfileCompletionService
|
||||||
|
|
||||||
|
@if (ShowProfileCompletionPrompt)
|
||||||
|
{
|
||||||
|
<div class="profile-completion-banner">
|
||||||
|
<span>Please complete your profile (first name, last name, and address).</span>
|
||||||
|
<a href="/Account/Manage" class="profile-completion-link">Complete profile</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<nav class="top-bar">
|
<nav class="top-bar">
|
||||||
<div class="top-bar-left">
|
<div class="top-bar-left">
|
||||||
@@ -40,16 +52,34 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string currentUrl = "";
|
private string currentUrl = "";
|
||||||
|
private bool ShowProfileCompletionPrompt { get; set; }
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
currentUrl = GetRelativePath(NavigationManager.Uri);
|
currentUrl = GetRelativePath(NavigationManager.Uri);
|
||||||
NavigationManager.LocationChanged += OnLocationChanged;
|
NavigationManager.LocationChanged += OnLocationChanged;
|
||||||
|
|
||||||
|
await RefreshProfileCompletionPromptAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||||
{
|
{
|
||||||
currentUrl = GetRelativePath(e.Location);
|
currentUrl = GetRelativePath(e.Location);
|
||||||
|
await RefreshProfileCompletionPromptAsync().ConfigureAwait(false);
|
||||||
|
await InvokeAsync(StateHasChanged).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshProfileCompletionPromptAsync()
|
||||||
|
{
|
||||||
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
ShowProfileCompletionPrompt = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isComplete = await ProfileCompletionService.IsProfileCompleteAsync(userId).ConfigureAwait(false);
|
||||||
|
ShowProfileCompletionPrompt = !isComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetRelativePath(string absoluteUri)
|
private string GetRelativePath(string absoluteUri)
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
.profile-completion-banner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #664d03;
|
||||||
|
border-bottom: 1px solid #ffecb5;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-completion-link {
|
||||||
|
color: #664d03;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-completion-link:hover {
|
||||||
|
color: #523d02;
|
||||||
|
}
|
||||||
|
|
||||||
.top-bar {
|
.top-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -5,5 +5,8 @@ namespace BirthList.Web.Data;
|
|||||||
// Add profile data for application users by adding properties to the ApplicationUser class
|
// Add profile data for application users by adding properties to the ApplicationUser class
|
||||||
public class ApplicationUser : IdentityUser
|
public class ApplicationUser : IdentityUser
|
||||||
{
|
{
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public string? Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+288
@@ -0,0 +1,288 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BirthList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Web.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260518213355_AddUserProfileFields")]
|
||||||
|
partial class AddUserProfileFields
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.26")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Web.Data.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("BirthList.Web.Data.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Web.Data.ApplicationUser", 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("BirthList.Web.Data.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Web.Data.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserProfileFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Address",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FirstName",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "LastName",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Address",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FirstName",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastName",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using BirthList.Web.Data;
|
using BirthList.Web.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -15,7 +16,11 @@ namespace BirthList.Web.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.26")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("BirthList.Web.Data.ApplicationUser", b =>
|
modelBuilder.Entity("BirthList.Web.Data.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
@@ -25,6 +30,9 @@ namespace BirthList.Web.Migrations
|
|||||||
b.Property<int>("AccessFailedCount")
|
b.Property<int>("AccessFailedCount")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
b.Property<string>("ConcurrencyStamp")
|
||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
@@ -36,6 +44,12 @@ namespace BirthList.Web.Migrations
|
|||||||
b.Property<bool>("EmailConfirmed")
|
b.Property<bool>("EmailConfirmed")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<bool>("LockoutEnabled")
|
b.Property<bool>("LockoutEnabled")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
@@ -115,6 +129,8 @@ namespace BirthList.Web.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
b.Property<string>("ClaimType")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
@@ -138,6 +154,8 @@ namespace BirthList.Web.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
b.Property<string>("ClaimType")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
|||||||
@@ -95,18 +95,22 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
|||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
var usersById = await applicationDbContext.Users
|
var users = await applicationDbContext.Users
|
||||||
.Where(x => admins.Contains(x.Id))
|
.Where(x => admins.Contains(x.Id))
|
||||||
.Select(x => new { x.Id, x.Email })
|
.Select(x => new { x.Id, x.Email, x.FirstName, x.LastName })
|
||||||
.ToDictionaryAsync(x => x.Id, x => x.Email, cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var usersById = users.ToDictionary(
|
||||||
|
x => x.Id,
|
||||||
|
x => BuildUserDisplayName(x.FirstName, x.LastName, x.Email, x.Id));
|
||||||
|
|
||||||
return admins
|
return admins
|
||||||
.Select(userId => new RegistryAdminDisplayModel
|
.Select(userId => new RegistryAdminDisplayModel
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
DisplayName = usersById.TryGetValue(userId, out var email) && !string.IsNullOrWhiteSpace(email)
|
DisplayName = usersById.TryGetValue(userId, out var displayName)
|
||||||
? email
|
? displayName
|
||||||
: userId
|
: userId
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -180,12 +184,16 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
|||||||
userIds.Add(contribution.UserId);
|
userIds.Add(contribution.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersById = await applicationDbContext.Users
|
var users = await applicationDbContext.Users
|
||||||
.Where(x => userIds.Contains(x.Id))
|
.Where(x => userIds.Contains(x.Id))
|
||||||
.Select(x => new { x.Id, x.Email })
|
.Select(x => new { x.Id, x.Email, x.FirstName, x.LastName })
|
||||||
.ToDictionaryAsync(x => x.Id, x => x.Email ?? x.Id, cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var usersById = users.ToDictionary(
|
||||||
|
x => x.Id,
|
||||||
|
x => BuildUserDisplayName(x.FirstName, x.LastName, x.Email, x.Id));
|
||||||
|
|
||||||
var purchasesByItemId = purchases
|
var purchasesByItemId = purchases
|
||||||
.GroupBy(x => x.RegistryItemId)
|
.GroupBy(x => x.RegistryItemId)
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
@@ -422,12 +430,16 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
|||||||
userIds.Add(contribution.UserId);
|
userIds.Add(contribution.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersById = await applicationDbContext.Users
|
var users = await applicationDbContext.Users
|
||||||
.Where(x => userIds.Contains(x.Id))
|
.Where(x => userIds.Contains(x.Id))
|
||||||
.Select(x => new { x.Id, x.Email })
|
.Select(x => new { x.Id, x.Email, x.FirstName, x.LastName })
|
||||||
.ToDictionaryAsync(x => x.Id, x => x.Email ?? x.Id, cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var usersById = users.ToDictionary(
|
||||||
|
x => x.Id,
|
||||||
|
x => BuildUserDisplayName(x.FirstName, x.LastName, x.Email, x.Id));
|
||||||
|
|
||||||
var purchasesByItemId = purchases
|
var purchasesByItemId = purchases
|
||||||
.GroupBy(x => x.RegistryItemId)
|
.GroupBy(x => x.RegistryItemId)
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
@@ -508,12 +520,16 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
|||||||
userIds.Add(contribution.UserId);
|
userIds.Add(contribution.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersById = await applicationDbContext.Users
|
var users = await applicationDbContext.Users
|
||||||
.Where(x => userIds.Contains(x.Id))
|
.Where(x => userIds.Contains(x.Id))
|
||||||
.Select(x => new { x.Id, x.Email })
|
.Select(x => new { x.Id, x.Email, x.FirstName, x.LastName })
|
||||||
.ToDictionaryAsync(x => x.Id, x => x.Email ?? x.Id, cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var usersById = users.ToDictionary(
|
||||||
|
x => x.Id,
|
||||||
|
x => BuildUserDisplayName(x.FirstName, x.LastName, x.Email, x.Id));
|
||||||
|
|
||||||
return new RegistryItemEditModel
|
return new RegistryItemEditModel
|
||||||
{
|
{
|
||||||
Id = item.Id,
|
Id = item.Id,
|
||||||
@@ -907,4 +923,20 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
|||||||
var fallback = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString("N")))).Substring(0, 16).ToLowerInvariant();
|
var fallback = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString("N")))).Substring(0, 16).ToLowerInvariant();
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildUserDisplayName(string? firstName, string? lastName, string? email, string userId)
|
||||||
|
{
|
||||||
|
var fullName = $"{firstName} {lastName}".Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(fullName))
|
||||||
|
{
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
|||||||
options.UseSqlite(connectionString);
|
options.UseSqlite(connectionString);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
|
||||||
|
{
|
||||||
|
if (string.Equals(dataProvider, "SqlServer", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
options.UseSqlServer(connectionString);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.UseSqlite(connectionString);
|
||||||
|
}, ServiceLifetime.Scoped);
|
||||||
|
|
||||||
builder.Services.AddDbContext<RegistryDbContext>(options =>
|
builder.Services.AddDbContext<RegistryDbContext>(options =>
|
||||||
{
|
{
|
||||||
if (string.Equals(dataProvider, "SqlServer", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(dataProvider, "SqlServer", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -113,6 +124,7 @@ builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
|||||||
|
|
||||||
builder.Services.AddScoped<SmtpEmailSender>();
|
builder.Services.AddScoped<SmtpEmailSender>();
|
||||||
builder.Services.AddScoped<IEmailSender<ApplicationUser>>(serviceProvider => serviceProvider.GetRequiredService<SmtpEmailSender>());
|
builder.Services.AddScoped<IEmailSender<ApplicationUser>>(serviceProvider => serviceProvider.GetRequiredService<SmtpEmailSender>());
|
||||||
|
builder.Services.AddScoped<ProfileCompletionService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using BirthList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BirthList.Web.Services;
|
||||||
|
|
||||||
|
internal sealed class ProfileCompletionService(IDbContextFactory<ApplicationDbContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
public async Task<bool> IsProfileCompleteAsync(string userId)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(userId);
|
||||||
|
|
||||||
|
await using var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var user = await dbContext.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Id == userId)
|
||||||
|
.Select(x => new { x.FirstName, x.LastName, x.Address })
|
||||||
|
.FirstOrDefaultAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !string.IsNullOrWhiteSpace(user.FirstName)
|
||||||
|
&& !string.IsNullOrWhiteSpace(user.LastName)
|
||||||
|
&& !string.IsNullOrWhiteSpace(user.Address);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user