Update registration

This commit is contained in:
2025-10-08 20:33:59 +09:00
parent 96172029d4
commit 257cd3e35e
16 changed files with 113 additions and 218 deletions

View File

@ -3,8 +3,7 @@
@implements IDisposable
@inject NavigationManager NavigationManager
<RadzenComponents @rendermode="InteractiveServer" />
@inject NotificationService NotificationService
<RadzenLayout Style="grid-template-areas: 'rz-sidebar rz-header' 'rz-sidebar rz-body'">
<RadzenHeader>
@ -25,11 +24,7 @@
<AuthorizeView>
<Authorized>
<RadzenPanelMenuItem Text="@context.User.Identity?.Name" />
<RadzenPanelMenuItem Path="javascript:document.forms['logout'].submit();" Text="Выйти" Icon="logout" />
<form action="account/logout" method="post" name="logout">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
</form>
<RadzenPanelMenuItem Path="/identity/logout" Text="Выйти" Icon="logout" />
</Authorized>
<NotAuthorized>
<RadzenPanelMenuItem Path="/account/register" Text="Регистрация" Icon="person_add" />
@ -40,6 +35,7 @@
</RadzenStack>
</RadzenSidebar>
<RadzenBody>
<RadzenNotification Style="position: absolute;top: 0; right: 0;" />
@Body
<div id="blazor-error-ui">
Произошла непредвиденная ошибка
@ -69,6 +65,8 @@
{
currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
NotificationService.Messages.Clear();
StateHasChanged();
}
}

View File

@ -1,21 +1,13 @@
@page "/account/register"
@using Microsoft.AspNetCore.Identity
@using Hcs.WebApp.Data
@using Hcs.WebApp.Identity
@inject IUserStore<AppUser> UserStore
@inject UserManager<AppUser> UserManager
@inject NotificationService NotificationService
@inject SignInManager<AppUser> SignInManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Регистрация аккаунта</PageTitle>
<RadzenCard class="rz-mx-auto" Style="max-width: 420px">
<RadzenTemplateForm TItem="InputModel" Data=@Input Method="post" Submit=@OnSubmit>
<RadzenTemplateForm TItem="InputModel" Data=@Input Method="post" Action="@($"identity/register?returnUrl={ReturnUrl}")">
<RadzenStack Gap="1rem" class="rz-p-sm-12">
<RadzenText TextStyle="TextStyle.H4" TextAlign="TextAlign.Center">Регистрация</RadzenText>
<RadzenText TextStyle="TextStyle.H5" TextAlign="TextAlign.Center">Регистрация</RadzenText>
<RadzenFormField Text="Логин" Variant="Variant.Outlined">
<ChildContent>
<RadzenTextBox Name="UserName" @bind-Value=@Input.UserName AutoCompleteType="AutoCompleteType.Username" />
@ -42,7 +34,7 @@
</ChildContent>
<Helper>
<RadzenRequiredValidator Component="ConfirmPassword" Text="Поле 'Пароль' обязательно к заполнению" />
<RadzenCompareValidator Visible=@(!string.IsNullOrEmpty(Input.ConfirmPassword)) Value=@Input.Password Component="ConfirmPassword" Text="Пароли должны совпадать" />
<RadzenCompareValidator Value=@Input.Password Component="ConfirmPassword" Text="Пароли должны совпадать" />
</Helper>
</RadzenFormField>
<RadzenButton ButtonType="ButtonType.Submit" Text="Зарегистрировать"></RadzenButton>
@ -63,28 +55,28 @@
[SupplyParameterFromForm]
InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
string? Errors { get; set; }
[SupplyParameterFromQuery]
string? ReturnUrl { get; set; }
async Task OnSubmit(InputModel mode)
protected override void OnAfterRender(bool firstRender)
{
var user = Activator.CreateInstance<AppUser>();
await UserStore.SetUserNameAsync(user, Input.UserName, CancellationToken.None);
base.OnAfterRender(firstRender);
var result = await UserManager.CreateAsync(user, Input.Password);
if (!result.Succeeded)
if (firstRender)
{
if (!string.IsNullOrEmpty(Errors))
{
NotificationService.Notify(new NotificationMessage()
{
Severity = NotificationSeverity.Error,
Summary = "Ошибка",
Detail = string.Join(", ", result.Errors.Select(error => error.Description))
Detail = Errors,
Duration = -1d
});
return;
}
await SignInManager.SignInAsync(user, isPersistent: false);
RedirectManager.RedirectTo(ReturnUrl);
}
}
}
}

View File

@ -1,12 +1,12 @@
@page "/test/export"
@using Microsoft.AspNetCore.Authorization
@using Hcs.Broker
@using Hcs.Broker.Logger
@using Hcs.Broker.MessageCapturer
@using Hcs.Service.Async.Nsi
@using Hcs.WebApp.Config
@using Hcs.WebApp.Utils
@using Microsoft.AspNetCore.Authorization
@implements IDisposable

View File

@ -1,5 +1,8 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Hcs.WebApp
@using Hcs.WebApp.Components
@using Hcs.WebApp.Components.Shared
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@ -8,8 +11,5 @@
@using Microsoft.JSInterop
@using Radzen
@using Radzen.Blazor
@using Hcs.WebApp
@using Hcs.WebApp.Components
@using Hcs.WebApp.Components.Shared
@using static Microsoft.AspNetCore.Components.Web.RenderMode

View File

@ -0,0 +1,55 @@
using Hcs.WebApp.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Radzen;
namespace Hcs.WebApp.Controllers
{
[Route("identity/[action]")]
public class IdentityController(
IUserStore<AppUser> userStore,
UserManager<AppUser> userManager,
SignInManager<AppUser> signInManager) : Controller
{
private readonly IUserStore<AppUser> userStore = userStore;
private readonly UserManager<AppUser> userManager = userManager;
private readonly SignInManager<AppUser> signInManager = signInManager;
[HttpPost]
public async Task<IActionResult> Register(string userName, string password, string returnUrl)
{
var user = Activator.CreateInstance<AppUser>();
await userStore.SetUserNameAsync(user, userName, CancellationToken.None);
var result = await userManager.CreateAsync(user, password);
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(error => error.Description));
if (!string.IsNullOrEmpty(returnUrl))
{
return Redirect($"/account/register?errors={errors}&returnUrl={Uri.EscapeDataString(returnUrl)}");
}
else
{
return Redirect($"/account/register?errors={errors}");
}
}
await signInManager.SignInAsync(user, isPersistent: false);
if (string.IsNullOrEmpty(returnUrl))
{
Redirect("/");
}
return Redirect(returnUrl);
}
public async Task<IActionResult> Logout()
{
await signInManager.SignOutAsync();
return Redirect("/");
}
}
}

View File

@ -3,5 +3,5 @@ using Microsoft.EntityFrameworkCore;
namespace Hcs.WebApp.Data
{
internal class AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options) : IdentityDbContext<AppUser>(options) { }
public class AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options) : IdentityDbContext<AppUser, AppRole, string>(options) { }
}

View File

@ -0,0 +1,6 @@
using Microsoft.AspNetCore.Identity;
namespace Hcs.WebApp.Data
{
public class AppRole : IdentityRole { }
}

View File

@ -2,5 +2,5 @@
namespace Hcs.WebApp.Data
{
internal class AppUser : IdentityUser { }
public class AppUser : IdentityUser { }
}

View File

@ -4,7 +4,7 @@
namespace Hcs.WebApp.Data.Migrations
{
/// <inheritdoc />
internal partial class CreateIdentitySchema : Migration
public partial class CreateIdentitySchema : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)

View File

@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Hcs.WebApp.Data.Migrations
{
[DbContext(typeof(AppIdentityDbContext))]
internal class AppIdentityDbContextModelSnapshot : ModelSnapshot
public class AppIdentityDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.20" />
<PackageReference Include="Microsoft.AspNetCore.HeaderPropagation" Version="8.0.20" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.20" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.20" />

View File

@ -1,27 +0,0 @@
using Hcs.WebApp.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace Hcs.WebApp.Identity
{
internal static class IdentityComponentsEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
var accountGroup = endpoints.MapGroup("/account");
accountGroup.MapPost("/logout", async (
ClaimsPrincipal user,
SignInManager<AppUser> signInManager,
[FromForm] string returnUrl) =>
{
await signInManager.SignOutAsync();
return TypedResults.LocalRedirect($"~/{returnUrl}");
});
return accountGroup;
}
}
}

View File

@ -1,62 +0,0 @@
using Microsoft.AspNetCore.Components;
using System.Diagnostics.CodeAnalysis;
namespace Hcs.WebApp.Identity
{
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
{
public const string STATUS_COOKIE_NAME = "Identity.StatusMessage";
private static readonly CookieBuilder statusCookieBuilder = new()
{
SameSite = SameSiteMode.Strict,
HttpOnly = true,
IsEssential = true,
MaxAge = TimeSpan.FromSeconds(5),
};
private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
[DoesNotReturn]
public void RedirectTo(string? uri)
{
uri ??= "";
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
{
uri = navigationManager.ToBaseRelativePath(uri);
}
navigationManager.NavigateTo(uri);
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} может быть использован только при статичном рендеринге");
}
[DoesNotReturn]
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)
{
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
RedirectTo(newUri);
}
[DoesNotReturn]
public void RedirectToWithStatus(string uri, string message, HttpContext context)
{
context.Response.Cookies.Append(STATUS_COOKIE_NAME, message, statusCookieBuilder.Build(context));
RedirectTo(uri);
}
[DoesNotReturn]
public void RedirectToCurrentPage()
{
RedirectTo(CurrentPath);
}
[DoesNotReturn]
public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
{
RedirectToWithStatus(CurrentPath, message, context);
}
}
}

View File

@ -1,45 +0,0 @@
using Hcs.WebApp.Data;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Security.Claims;
namespace Hcs.WebApp.Identity
{
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> options) : RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
await using var scope = scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<AppUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<AppUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user is null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
}

View File

@ -1,18 +0,0 @@
using Hcs.WebApp.Data;
using Microsoft.AspNetCore.Identity;
namespace Hcs.WebApp.Identity
{
internal sealed class IdentityUserAccessor(UserManager<AppUser> userManager, IdentityRedirectManager redirectManager)
{
public async Task<AppUser> GetRequiredUserAsync(HttpContext context)
{
var user = await userManager.GetUserAsync(context.User);
if (user is null)
{
redirectManager.RedirectToWithStatus("account/invalid_user", $"Ошибка: Не удалось загрузить пользователя с идентификатором '{userManager.GetUserId(context.User)}'", context);
}
return user;
}
}
}

View File

@ -1,7 +1,5 @@
using Hcs.WebApp.Components;
using Hcs.WebApp.Data;
using Hcs.WebApp.Identity;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Radzen;
@ -12,52 +10,49 @@ builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddControllers();
builder.Services.AddRadzenComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies();
builder.Services.AddHttpClient("Hcs.WebApp").AddHeaderPropagation(x => x.Headers.Add("Cookie"));
builder.Services.AddHeaderPropagation(x => x.Headers.Add("Cookie"));
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
var connectionString = builder.Configuration.GetConnectionString("IdentityConnection") ?? throw new InvalidOperationException("<22><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> 'IdentityConnection'");
builder.Services.AddDbContext<AppIdentityDbContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services
.AddIdentityCore<AppUser>()
.AddIdentity<AppUser, AppRole>(options =>
{
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequiredUniqueChars = 1;
})
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseHeaderPropagation();
app.UseRouting();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app
.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapAdditionalIdentityEndpoints();
app.Run();