+
+@code {
+ private bool _drawerOpen = true;
+}
+
+
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/Layout/NavMenu.razor b/IntegryControlPanel/IntegryControlPanel.Client/Layout/NavMenu.razor
new file mode 100644
index 0000000..800ed07
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/Layout/NavMenu.razor
@@ -0,0 +1,51 @@
+@implements IDisposable
+
+@inject NavigationManager NavigationManager
+
+
+ Home
+ Counter
+
+ Weather
+
+ Auth Required
+
+
+ @context.User.Identity?.Name
+
+
+
+ Register
+ Login
+
+
+
+
+
+@code {
+ private string? currentUrl;
+
+ protected override void OnInitialized()
+ {
+ currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
+ NavigationManager.LocationChanged += OnLocationChanged;
+ }
+
+ private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
+ {
+ currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
+ StateHasChanged();
+ }
+
+ public void Dispose()
+ {
+ NavigationManager.LocationChanged -= OnLocationChanged;
+ }
+}
+
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/Pages/Auth.razor b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Auth.razor
new file mode 100644
index 0000000..f147080
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Auth.razor
@@ -0,0 +1,13 @@
+@page "/auth"
+
+@using Microsoft.AspNetCore.Authorization
+
+@attribute [Authorize]
+
+Auth
+
+You are authenticated
+
+
+ Hello @context.User.Identity?.Name!
+
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/Pages/Counter.razor b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Counter.razor
new file mode 100644
index 0000000..83d09a7
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Counter.razor
@@ -0,0 +1,19 @@
+@page "/counter"
+
+
+Counter
+
+Counter
+
+Current count: @currentCount
+
+Click me
+
+@code {
+ private int currentCount = 0;
+
+ private void IncrementCount()
+ {
+ currentCount++;
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/Pages/Home.razor b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Home.razor
new file mode 100644
index 0000000..6aaa2a4
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Home.razor
@@ -0,0 +1,59 @@
+@page "/"
+
+Home
+
+Hello, world!
+Welcome to your new app, powered by MudBlazor and the .NET 9 Template!
+
+
+ You can find documentation and examples on our website here:
+
+ www.mudblazor.com
+
+
+
+
+Interactivity in this Template
+
+
+ When you opt for the "Global" Interactivity Location,
+ the render modes are defined in App.razor and consequently apply to all child components.
+ In this case, providers are globally set in the MainLayout.
+
+ On the other hand, if you choose the "Per page/component" Interactivity Location,
+ it is necessary to include the
+
+ <MudPopoverProvider />
+ <MudDialogProvider />
+ <MudSnackbarProvider />
+
+ components on every interactive page.
+
+ If a render mode is not specified for a page, it defaults to Server-Side Rendering (SSR),
+ similar to this page. While MudBlazor allows pages to be rendered in SSR,
+ please note that interactive features, such as buttons and dropdown menus, will not be functional.
+
+
+
+What's New in Blazor with the Release of .NET 9
+
+
+Prerendering
+
+ If you're exploring the features of .NET 9 Blazor, you might be pleasantly surprised to learn that each page is prerendered on the server, regardless of the selected render mode.
+ This means that you'll need to inject all necessary services on the server, even when opting for the wasm (WebAssembly) render mode.
+ This prerendering functionality is crucial to ensuring that WebAssembly mode feels fast and responsive, especially when it comes to initial page load times.
+ For more information on how to detect prerendering and leverage the RenderContext, you can refer to the following link:
+
+ More details
+
+
+
+
+InteractiveAuto
+
+ A discussion on how to achieve this can be found here:
+
+ More details
+
+
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/Pages/Weather.razor b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Weather.razor
new file mode 100644
index 0000000..3ffd7d8
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/Pages/Weather.razor
@@ -0,0 +1,60 @@
+@page "/weather"
+
+
+
+Weather
+
+Weather forecast
+This component demonstrates fetching data from the server.
+
+@if (forecasts == null)
+{
+
+}
+else
+{
+
+
+ Date
+ Temp. (C)
+ Temp. (F)
+ Summary
+
+
+ @context.Date
+ @context.TemperatureC
+ @context.TemperatureF
+ @context.Summary
+
+
+
+
+
+}
+
+@code {
+ private WeatherForecast[]? forecasts;
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Simulate asynchronous loading to demonstrate a loading indicator
+ await Task.Delay(500);
+
+ var startDate = DateOnly.FromDateTime(DateTime.Now);
+ var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
+ forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
+ {
+ Date = startDate.AddDays(index),
+ TemperatureC = Random.Shared.Next(-20, 55),
+ Summary = summaries[Random.Shared.Next(summaries.Length)]
+ }).ToArray();
+ }
+
+ private class WeatherForecast
+ {
+ public DateOnly Date { get; set; }
+ public int TemperatureC { get; set; }
+ public string? Summary { get; set; }
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/Program.cs b/IntegryControlPanel/IntegryControlPanel.Client/Program.cs
new file mode 100644
index 0000000..14397bc
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/Program.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using MudBlazor.Services;
+
+var builder = WebAssemblyHostBuilder.CreateDefault(args);
+
+builder.Services.AddMudServices();
+
+builder.Services.AddAuthorizationCore();
+builder.Services.AddCascadingAuthenticationState();
+builder.Services.AddAuthenticationStateDeserialization();
+
+await builder.Build().RunAsync();
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/RedirectToLogin.razor b/IntegryControlPanel/IntegryControlPanel.Client/RedirectToLogin.razor
new file mode 100644
index 0000000..c8b8eff
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/RedirectToLogin.razor
@@ -0,0 +1,8 @@
+@inject NavigationManager NavigationManager
+
+@code {
+ protected override void OnInitialized()
+ {
+ NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/Routes.razor b/IntegryControlPanel/IntegryControlPanel.Client/Routes.razor
new file mode 100644
index 0000000..16f0026
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/Routes.razor
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/_Imports.razor b/IntegryControlPanel/IntegryControlPanel.Client/_Imports.razor
new file mode 100644
index 0000000..bf18238
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/_Imports.razor
@@ -0,0 +1,12 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Authorization
+@using MudBlazor.StaticInput
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using MudBlazor
+@using MudBlazor.Services
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/wwwroot/appsettings.Development.json b/IntegryControlPanel/IntegryControlPanel.Client/wwwroot/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/wwwroot/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel.Client/wwwroot/appsettings.json b/IntegryControlPanel/IntegryControlPanel.Client/wwwroot/appsettings.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel.Client/wwwroot/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000..f183858
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs
@@ -0,0 +1,113 @@
+using System.Security.Claims;
+using System.Text.Json;
+using IntegryControlPanel.Components.Account.Pages;
+using IntegryControlPanel.Components.Account.Pages.Manage;
+using IntegryControlPanel.Data;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ internal static class IdentityComponentsEndpointRouteBuilderExtensions
+ {
+ // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
+ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+
+ var accountGroup = endpoints.MapGroup("/Account");
+
+ accountGroup.MapPost("/PerformExternalLogin", (
+ HttpContext context,
+ [FromServices] SignInManager signInManager,
+ [FromForm] string provider,
+ [FromForm] string returnUrl) =>
+ {
+ IEnumerable> query = [
+ new("ReturnUrl", returnUrl),
+ new("Action", ExternalLogin.LoginCallbackAction)];
+
+ var redirectUrl = UriHelper.BuildRelative(
+ context.Request.PathBase,
+ "/Account/ExternalLogin",
+ QueryString.Create(query));
+
+ var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
+ return TypedResults.Challenge(properties, [provider]);
+ });
+
+ accountGroup.MapPost("/Logout", async (
+ ClaimsPrincipal user,
+ [FromServices] SignInManager signInManager,
+ [FromForm] string returnUrl) =>
+ {
+ await signInManager.SignOutAsync();
+ return TypedResults.LocalRedirect($"~/{returnUrl}");
+ });
+
+ var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization();
+
+ manageGroup.MapPost("/LinkExternalLogin", async (
+ HttpContext context,
+ [FromServices] SignInManager signInManager,
+ [FromForm] string provider) =>
+ {
+ // Clear the existing external cookie to ensure a clean login process
+ await context.SignOutAsync(IdentityConstants.ExternalScheme);
+
+ var redirectUrl = UriHelper.BuildRelative(
+ context.Request.PathBase,
+ "/Account/Manage/ExternalLogins",
+ QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction));
+
+ var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User));
+ return TypedResults.Challenge(properties, [provider]);
+ });
+
+ var loggerFactory = endpoints.ServiceProvider.GetRequiredService();
+ var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData");
+
+ manageGroup.MapPost("/DownloadPersonalData", async (
+ HttpContext context,
+ [FromServices] UserManager userManager,
+ [FromServices] AuthenticationStateProvider authenticationStateProvider) =>
+ {
+ var user = await userManager.GetUserAsync(context.User);
+ if (user is null)
+ {
+ return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'.");
+ }
+
+ var userId = await userManager.GetUserIdAsync(user);
+ downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId);
+
+ // Only include personal data for download
+ var personalData = new Dictionary();
+ var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
+ prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
+ foreach (var p in personalDataProps)
+ {
+ personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
+ }
+
+ var logins = await userManager.GetLoginsAsync(user);
+ foreach (var l in logins)
+ {
+ personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
+ }
+
+ personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!);
+ var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData);
+
+ context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json");
+ return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json");
+ });
+
+ return accountGroup;
+ }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityNoOpEmailSender.cs b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityNoOpEmailSender.cs
new file mode 100644
index 0000000..51f48b1
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityNoOpEmailSender.cs
@@ -0,0 +1,21 @@
+using IntegryControlPanel.Data;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+
+namespace IntegryControlPanel.Components.Account
+{
+ // Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
+ internal sealed class IdentityNoOpEmailSender : IEmailSender
+ {
+ private readonly IEmailSender emailSender = new NoOpEmailSender();
+
+ public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
+ emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here.");
+
+ public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
+ emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here.");
+
+ public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
+ emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityRedirectManager.cs b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityRedirectManager.cs
new file mode 100644
index 0000000..3f78756
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityRedirectManager.cs
@@ -0,0 +1,59 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+
+namespace IntegryControlPanel.Components.Account
+{
+ internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
+ {
+ public const string StatusCookieName = "Identity.StatusMessage";
+
+ private static readonly CookieBuilder StatusCookieBuilder = new()
+ {
+ SameSite = SameSiteMode.Strict,
+ HttpOnly = true,
+ IsEssential = true,
+ MaxAge = TimeSpan.FromSeconds(5),
+ };
+
+ [DoesNotReturn]
+ public void RedirectTo(string? uri)
+ {
+ uri ??= "";
+
+ // Prevent open redirects.
+ if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
+ {
+ uri = navigationManager.ToBaseRelativePath(uri);
+ }
+
+ // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect.
+ // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown.
+ navigationManager.NavigateTo(uri);
+ throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
+ }
+
+ [DoesNotReturn]
+ public void RedirectTo(string uri, Dictionary 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(StatusCookieName, message, StatusCookieBuilder.Build(context));
+ RedirectTo(uri);
+ }
+
+ private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
+
+ [DoesNotReturn]
+ public void RedirectToCurrentPage() => RedirectTo(CurrentPath);
+
+ [DoesNotReturn]
+ public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
+ => RedirectToWithStatus(CurrentPath, message, context);
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityUserAccessor.cs b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityUserAccessor.cs
new file mode 100644
index 0000000..aa3f363
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/IdentityUserAccessor.cs
@@ -0,0 +1,20 @@
+using IntegryControlPanel.Data;
+using Microsoft.AspNetCore.Identity;
+
+namespace IntegryControlPanel.Components.Account
+{
+ internal sealed class IdentityUserAccessor(UserManager userManager, IdentityRedirectManager redirectManager)
+ {
+ public async Task GetRequiredUserAsync(HttpContext context)
+ {
+ var user = await userManager.GetUserAsync(context.User);
+
+ if (user is null)
+ {
+ redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
+ }
+
+ return user;
+ }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/AccessDenied.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/AccessDenied.razor
new file mode 100644
index 0000000..db67a6f
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/AccessDenied.razor
@@ -0,0 +1,5 @@
+@page "/Account/AccessDenied"
+
+Access denied
+
+You do not have access to this resource.
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ConfirmEmail.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ConfirmEmail.razor
new file mode 100644
index 0000000..dcfe8f3
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ConfirmEmail.razor
@@ -0,0 +1,48 @@
+@page "/Account/ConfirmEmail"
+
+@using System.Text
+@using Microsoft.AspNetCore.Identity
+@using Microsoft.AspNetCore.WebUtilities
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject IdentityRedirectManager RedirectManager
+
+Confirm email
+
+
Confirm email
+
+
+@code {
+ private string? statusMessage;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromQuery]
+ private string? UserId { get; set; }
+
+ [SupplyParameterFromQuery]
+ private string? Code { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (UserId is null || Code is null)
+ {
+ RedirectManager.RedirectTo("");
+ }
+
+ var user = await UserManager.FindByIdAsync(UserId);
+ if (user is null)
+ {
+ HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
+ statusMessage = $"Error loading user with ID {UserId}";
+ }
+ else
+ {
+ var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
+ var result = await UserManager.ConfirmEmailAsync(user, code);
+ statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
+ }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ConfirmEmailChange.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ConfirmEmailChange.razor
new file mode 100644
index 0000000..3b76728
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ConfirmEmailChange.razor
@@ -0,0 +1,68 @@
+@page "/Account/ConfirmEmailChange"
+
+@using System.Text
+@using Microsoft.AspNetCore.Identity
+@using Microsoft.AspNetCore.WebUtilities
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityRedirectManager RedirectManager
+
+Confirm email change
+
+
Confirm email change
+
+
+
+@code {
+ private string? message;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromQuery]
+ private string? UserId { get; set; }
+
+ [SupplyParameterFromQuery]
+ private string? Email { get; set; }
+
+ [SupplyParameterFromQuery]
+ private string? Code { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (UserId is null || Email is null || Code is null)
+ {
+ RedirectManager.RedirectToWithStatus(
+ "Account/Login", "Error: Invalid email change confirmation link.", HttpContext);
+ }
+
+ var user = await UserManager.FindByIdAsync(UserId);
+ if (user is null)
+ {
+ message = "Unable to find user with Id '{userId}'";
+ return;
+ }
+
+ var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
+ var result = await UserManager.ChangeEmailAsync(user, Email, code);
+ if (!result.Succeeded)
+ {
+ message = "Error changing email.";
+ return;
+ }
+
+ // In our UI email and user name are one and the same, so when we update the email
+ // we need to update the user name.
+ var setUserNameResult = await UserManager.SetUserNameAsync(user, Email);
+ if (!setUserNameResult.Succeeded)
+ {
+ message = "Error changing user name.";
+ return;
+ }
+
+ await SignInManager.RefreshSignInAsync(user);
+ message = "Thank you for confirming your email change.";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ExternalLogin.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ExternalLogin.razor
new file mode 100644
index 0000000..cdcba73
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ExternalLogin.razor
@@ -0,0 +1,205 @@
+@page "/Account/ExternalLogin"
+
+@using System.ComponentModel.DataAnnotations
+@using System.Security.Claims
+@using System.Text
+@using System.Text.Encodings.Web
+@using Microsoft.AspNetCore.Identity
+@using Microsoft.AspNetCore.WebUtilities
+@using IntegryControlPanel.Data
+
+@inject SignInManager SignInManager
+@inject UserManager UserManager
+@inject IUserStore UserStore
+@inject IEmailSender EmailSender
+@inject NavigationManager NavigationManager
+@inject IdentityRedirectManager RedirectManager
+@inject ILogger Logger
+
+Register
+
+
+
Register
+
Associate your @ProviderDisplayName account.
+
+
+
+ You've successfully authenticated with @ProviderDisplayName.
+ Please enter an email address for this site below and click the Register button to finish
+ logging in.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ public const string LoginCallbackAction = "LoginCallback";
+
+ private string? message;
+ private ExternalLoginInfo? externalLoginInfo;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ [SupplyParameterFromQuery]
+ private string? RemoteError { get; set; }
+
+ [SupplyParameterFromQuery]
+ private string? ReturnUrl { get; set; }
+
+ [SupplyParameterFromQuery]
+ private string? Action { get; set; }
+
+ private string? ProviderDisplayName => externalLoginInfo?.ProviderDisplayName;
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (RemoteError is not null)
+ {
+ RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
+ }
+
+ var info = await SignInManager.GetExternalLoginInfoAsync();
+ if (info is null)
+ {
+ RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
+ }
+
+ externalLoginInfo = info;
+
+ if (HttpMethods.IsGet(HttpContext.Request.Method))
+ {
+ if (Action == LoginCallbackAction)
+ {
+ await OnLoginCallbackAsync();
+ return;
+ }
+
+ // We should only reach this page via the login callback, so redirect back to
+ // the login page if we get here some other way.
+ RedirectManager.RedirectTo("Account/Login");
+ }
+ }
+
+ private async Task OnLoginCallbackAsync()
+ {
+ if (externalLoginInfo is null)
+ {
+ RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
+ }
+
+ // Sign in the user with this external login provider if the user already has a login.
+ var result = await SignInManager.ExternalLoginSignInAsync(
+ externalLoginInfo!.LoginProvider,
+ externalLoginInfo!.ProviderKey,
+ isPersistent: false,
+ bypassTwoFactor: true);
+
+ if (result.Succeeded)
+ {
+ Logger.LogInformation(
+ "{Name} logged in with {LoginProvider} provider.",
+ externalLoginInfo.Principal.Identity?.Name,
+ externalLoginInfo.LoginProvider);
+ RedirectManager.RedirectTo(ReturnUrl);
+ }
+ else if (result.IsLockedOut)
+ {
+ RedirectManager.RedirectTo("Account/Lockout");
+ }
+
+ // If the user does not have an account, then ask the user to create an account.
+ if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
+ {
+ Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
+ }
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ if (externalLoginInfo is null)
+ {
+ RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information during confirmation.", HttpContext);
+ }
+
+ var emailStore = GetEmailStore();
+ var user = CreateUser();
+
+ await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
+ await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
+
+ var result = await UserManager.CreateAsync(user);
+ if (result.Succeeded)
+ {
+ result = await UserManager.AddLoginAsync(user, externalLoginInfo);
+ if (result.Succeeded)
+ {
+ Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider);
+
+ var userId = await UserManager.GetUserIdAsync(user);
+ var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+
+ var callbackUrl = NavigationManager.GetUriWithQueryParameters(
+ NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
+ new Dictionary { ["userId"] = userId, ["code"] = code });
+ await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
+
+ // If account confirmation is required, we need to show the link if we don't have a real email sender
+ if (UserManager.Options.SignIn.RequireConfirmedAccount)
+ {
+ RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email });
+ }
+
+ await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider);
+ RedirectManager.RedirectTo(ReturnUrl);
+ }
+ }
+
+ message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
+ }
+
+ private static ApplicationUser CreateUser()
+ {
+ try
+ {
+ return Activator.CreateInstance();
+ }
+ catch
+ {
+ throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
+ $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
+ }
+ }
+
+ private IUserEmailStore GetEmailStore()
+ {
+ if (!UserManager.SupportsUserEmail)
+ {
+ throw new NotSupportedException("The default UI requires a user store with email support.");
+ }
+ return (IUserEmailStore)UserStore;
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; } = "";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ForgotPassword.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ForgotPassword.razor
new file mode 100644
index 0000000..4c5fe48
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ForgotPassword.razor
@@ -0,0 +1,68 @@
+@page "/Account/ForgotPassword"
+
+@using System.ComponentModel.DataAnnotations
+@using System.Text
+@using System.Text.Encodings.Web
+@using Microsoft.AspNetCore.Identity
+@using Microsoft.AspNetCore.WebUtilities
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject IEmailSender EmailSender
+@inject NavigationManager NavigationManager
+@inject IdentityRedirectManager RedirectManager
+
+Forgot your password?
+
+Forgot your password?
+Enter your email.
+
+
+
+
+
+
+
+
+
+
+ Reset password
+
+
+
+
+@code {
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ private async Task OnValidSubmitAsync()
+ {
+ var user = await UserManager.FindByEmailAsync(Input.Email);
+ if (user is null || !(await UserManager.IsEmailConfirmedAsync(user)))
+ {
+ // Don't reveal that the user does not exist or is not confirmed
+ RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
+ }
+
+ // For more information on how to enable account confirmation and password reset please
+ // visit https://go.microsoft.com/fwlink/?LinkID=532713
+ var code = await UserManager.GeneratePasswordResetTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = NavigationManager.GetUriWithQueryParameters(
+ NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri,
+ new Dictionary { ["code"] = code });
+
+ await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
+
+ RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; } = "";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ForgotPasswordConfirmation.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ForgotPasswordConfirmation.razor
new file mode 100644
index 0000000..31f70ce
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/ForgotPasswordConfirmation.razor
@@ -0,0 +1,7 @@
+@page "/Account/ForgotPasswordConfirmation"
+
+Forgot password confirmation
+
+Forgot password confirmation
+
+Please check your email to reset your password.
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/InvalidPasswordReset.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/InvalidPasswordReset.razor
new file mode 100644
index 0000000..561b651
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/InvalidPasswordReset.razor
@@ -0,0 +1,8 @@
+@page "/Account/InvalidPasswordReset"
+
+Invalid password reset
+
+
Invalid password reset
+
+ The password reset link is invalid.
+
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/InvalidUser.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/InvalidUser.razor
new file mode 100644
index 0000000..e61fe5d
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/InvalidUser.razor
@@ -0,0 +1,7 @@
+@page "/Account/InvalidUser"
+
+Invalid user
+
+
Invalid user
+
+
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Lockout.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Lockout.razor
new file mode 100644
index 0000000..017e31d
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Lockout.razor
@@ -0,0 +1,8 @@
+@page "/Account/Lockout"
+
+Locked out
+
+
+
Locked out
+
This account has been locked out, please try again later.
+
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Login.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Login.razor
new file mode 100644
index 0000000..f8fd7a3
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Login.razor
@@ -0,0 +1,121 @@
+@page "/Account/Login"
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Authentication
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject SignInManager SignInManager
+@inject ILogger Logger
+@inject NavigationManager NavigationManager
+@inject IdentityRedirectManager RedirectManager
+
+Log in
+
+Log in
+
+
+
+
+
+
+
+ Use a local account to log in.
+
+
+
+
+
+
+
+
+
+ Remember me
+
+
+ Log in
+
+
+
+
+
+
+ Forgot your password?
+ { ["ReturnUrl"] = ReturnUrl }))">Register as a new user
+ Resend email confirmation
+
+
+
+
+ Use another service to log in.
+
+
+
+
+
+@code {
+ private string? errorMessage;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ [SupplyParameterFromQuery]
+ private string? ReturnUrl { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (HttpMethods.IsGet(HttpContext.Request.Method))
+ {
+ // Clear the existing external cookie to ensure a clean login process
+ await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
+ }
+ }
+
+ public async Task LoginUser()
+ {
+ // This doesn't count login failures towards account lockout
+ // To enable password failures to trigger account lockout, set lockoutOnFailure: true
+ var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
+ if (result.Succeeded)
+ {
+ Logger.LogInformation("User logged in.");
+ RedirectManager.RedirectTo(ReturnUrl);
+ }
+ else if (result.RequiresTwoFactor)
+ {
+ RedirectManager.RedirectTo(
+ "Account/LoginWith2fa",
+ new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
+ }
+ else if (result.IsLockedOut)
+ {
+ Logger.LogWarning("User account locked out.");
+ RedirectManager.RedirectTo("Account/Lockout");
+ }
+ else
+ {
+ errorMessage = "Error: Invalid login attempt.";
+ }
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; } = "";
+
+ [Required]
+ [DataType(DataType.Password)]
+ public string Password { get; set; } = "";
+
+ [Display(Name = "Remember me?")]
+ public bool RememberMe { get; set; }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/LoginWith2fa.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/LoginWith2fa.razor
new file mode 100644
index 0000000..ad9ee2d
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/LoginWith2fa.razor
@@ -0,0 +1,101 @@
+@page "/Account/LoginWith2fa"
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject SignInManager SignInManager
+@inject UserManager UserManager
+@inject IdentityRedirectManager RedirectManager
+@inject ILogger Logger
+
+Two-factor authentication
+
+
Two-factor authentication
+
+
+
Your login is protected with an authenticator app. Enter your authenticator code below.
+
+@code {
+ private string? message;
+ private ApplicationUser user = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ [SupplyParameterFromQuery]
+ private string? ReturnUrl { get; set; }
+
+ [SupplyParameterFromQuery]
+ private bool RememberMe { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Ensure the user has gone through the username & password screen first
+ user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
+ throw new InvalidOperationException("Unable to load two-factor authentication user.");
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty);
+ var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine);
+ var userId = await UserManager.GetUserIdAsync(user);
+
+ if (result.Succeeded)
+ {
+ Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId);
+ RedirectManager.RedirectTo(ReturnUrl);
+ }
+ else if (result.IsLockedOut)
+ {
+ Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
+ RedirectManager.RedirectTo("Account/Lockout");
+ }
+ else
+ {
+ Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId);
+ message = "Error: Invalid authenticator code.";
+ }
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Authenticator code")]
+ public string? TwoFactorCode { get; set; }
+
+ [Display(Name = "Remember this machine")]
+ public bool RememberMachine { get; set; }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/LoginWithRecoveryCode.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/LoginWithRecoveryCode.razor
new file mode 100644
index 0000000..51fff5f
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/LoginWithRecoveryCode.razor
@@ -0,0 +1,85 @@
+@page "/Account/LoginWithRecoveryCode"
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject SignInManager SignInManager
+@inject UserManager UserManager
+@inject IdentityRedirectManager RedirectManager
+@inject ILogger Logger
+
+Recovery code verification
+
+
Recovery code verification
+
+
+
+ You have requested to log in with a recovery code. This login will not be remembered until you provide
+ an authenticator app code at log in or disable 2FA and log in again.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ private string? message;
+ private ApplicationUser user = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ [SupplyParameterFromQuery]
+ private string? ReturnUrl { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Ensure the user has gone through the username & password screen first
+ user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
+ throw new InvalidOperationException("Unable to load two-factor authentication user.");
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
+
+ var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
+
+ var userId = await UserManager.GetUserIdAsync(user);
+
+ if (result.Succeeded)
+ {
+ Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId);
+ RedirectManager.RedirectTo(ReturnUrl);
+ }
+ else if (result.IsLockedOut)
+ {
+ Logger.LogWarning("User account locked out.");
+ RedirectManager.RedirectTo("Account/Lockout");
+ }
+ else
+ {
+ Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId);
+ message = "Error: Invalid recovery code entered.";
+ }
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [DataType(DataType.Text)]
+ [Display(Name = "Recovery Code")]
+ public string RecoveryCode { get; set; } = "";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ChangePassword.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ChangePassword.razor
new file mode 100644
index 0000000..05e2f95
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ChangePassword.razor
@@ -0,0 +1,98 @@
+@page "/Account/Manage/ChangePassword"
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityUserAccessor UserAccessor
+@inject IdentityRedirectManager RedirectManager
+@inject ILogger Logger
+
+Change password
+
+Change password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update password
+
+
+
+
+@code {
+ private string? message;
+ private ApplicationUser user = default!;
+ private bool hasPassword;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+ hasPassword = await UserManager.HasPasswordAsync(user);
+ if (!hasPassword)
+ {
+ RedirectManager.RedirectTo("Account/Manage/SetPassword");
+ }
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
+ if (!changePasswordResult.Succeeded)
+ {
+ message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}";
+ return;
+ }
+
+ await SignInManager.RefreshSignInAsync(user);
+ Logger.LogInformation("User changed their password successfully.");
+
+ RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext);
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [DataType(DataType.Password)]
+ [Display(Name = "Current password")]
+ public string OldPassword { get; set; } = "";
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ [Display(Name = "New password")]
+ public string NewPassword { get; set; } = "";
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm new password")]
+ [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
+ public string ConfirmPassword { get; set; } = "";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/DeletePersonalData.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/DeletePersonalData.razor
new file mode 100644
index 0000000..b506639
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/DeletePersonalData.razor
@@ -0,0 +1,86 @@
+@page "/Account/Manage/DeletePersonalData"
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityUserAccessor UserAccessor
+@inject IdentityRedirectManager RedirectManager
+@inject ILogger Logger
+
+Delete Personal Data
+
+Delete personal data
+
+
+
+
+ Deleting this data will permanently remove your account, and this cannot be recovered.
+
+
+
+
+
+
+ @if (requirePassword)
+ {
+
+
+
+ }
+
+ Delete data and close my account
+
+
+
+
+@code {
+ private string? message;
+ private ApplicationUser user = default!;
+ private bool requirePassword;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ Input ??= new();
+ user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+ requirePassword = await UserManager.HasPasswordAsync(user);
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password))
+ {
+ message = "Error: Incorrect password.";
+ return;
+ }
+
+ var result = await UserManager.DeleteAsync(user);
+ if (!result.Succeeded)
+ {
+ throw new InvalidOperationException("Unexpected error occurred deleting user.");
+ }
+
+ await SignInManager.SignOutAsync();
+
+ var userId = await UserManager.GetUserIdAsync(user);
+ Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
+
+ RedirectManager.RedirectToCurrentPage();
+ }
+
+ private sealed class InputModel
+ {
+ [DataType(DataType.Password)]
+ public string Password { get; set; } = "";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/Disable2fa.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/Disable2fa.razor
new file mode 100644
index 0000000..d515f06
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/Disable2fa.razor
@@ -0,0 +1,64 @@
+@page "/Account/Manage/Disable2fa"
+
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject IdentityUserAccessor UserAccessor
+@inject IdentityRedirectManager RedirectManager
+@inject ILogger Logger
+
+Disable two-factor authentication (2FA)
+
+
+
Disable two-factor authentication (2FA)
+
+
+
+ This action only disables 2FA.
+
+
+ Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
+ used in an authenticator app you should reset your authenticator keys.
+
+
+ Download a two-factor authenticator app like Microsoft Authenticator for
+ Android and
+ iOS or
+ Google Authenticator for
+ Android and
+ iOS.
+
+
+
+
+ Scan the QR Code or enter this key into your two factor authenticator app. Spaces and casing do not matter:
+
+
+ @sharedKey
+
+
+ Learn how to enable QR code generation.
+
+
+
+
+
+
+ Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
+ with a unique code. Enter the code in the confirmation box below.
+
+
+
+
+
+
+
+
+
+ Verify
+
+
+
+
+
+}
+
+@code {
+ private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
+
+ private string? message;
+ private ApplicationUser user = default!;
+ private string? sharedKey;
+ private string? authenticatorUri;
+ private IEnumerable? recoveryCodes;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+
+ await LoadSharedKeyAndQrCodeUriAsync(user);
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ // Strip spaces and hyphens
+ var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
+
+ var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync(
+ user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
+
+ if (!is2faTokenValid)
+ {
+ message = "Error: Verification code is invalid.";
+ return;
+ }
+
+ await UserManager.SetTwoFactorEnabledAsync(user, true);
+ var userId = await UserManager.GetUserIdAsync(user);
+ Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
+
+ message = "Your authenticator app has been verified.";
+
+ if (await UserManager.CountRecoveryCodesAsync(user) == 0)
+ {
+ recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
+ }
+ else
+ {
+ RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext);
+ }
+ }
+
+ private async ValueTask LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)
+ {
+ // Load the authenticator key & QR code URI to display on the form
+ var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
+ if (string.IsNullOrEmpty(unformattedKey))
+ {
+ await UserManager.ResetAuthenticatorKeyAsync(user);
+ unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
+ }
+
+ sharedKey = FormatKey(unformattedKey!);
+
+ var email = await UserManager.GetEmailAsync(user);
+ authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
+ }
+
+ private string FormatKey(string unformattedKey)
+ {
+ var result = new StringBuilder();
+ int currentPosition = 0;
+ while (currentPosition + 4 < unformattedKey.Length)
+ {
+ result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
+ currentPosition += 4;
+ }
+ if (currentPosition < unformattedKey.Length)
+ {
+ result.Append(unformattedKey.AsSpan(currentPosition));
+ }
+
+ return result.ToString().ToLowerInvariant();
+ }
+
+ private string GenerateQrCodeUri(string email, string unformattedKey)
+ {
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ AuthenticatorUriFormat,
+ UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
+ UrlEncoder.Encode(email),
+ unformattedKey);
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Verification Code")]
+ public string Code { get; set; } = "";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ExternalLogins.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ExternalLogins.razor
new file mode 100644
index 0000000..eb8cc63
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ExternalLogins.razor
@@ -0,0 +1,140 @@
+@page "/Account/Manage/ExternalLogins"
+
+@using Microsoft.AspNetCore.Authentication
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityUserAccessor UserAccessor
+@inject IUserStore UserStore
+@inject IdentityRedirectManager RedirectManager
+
+Manage your external logins
+
+
+@if (currentLogins?.Count > 0)
+{
+
+ If you lose your device and don't have the recovery codes you will lose access to your account.
+
+
+ Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
+ used in an authenticator app you should reset your authenticator keys.
+
+
+
+
+
+}
+
+@code {
+ private string? message;
+ private ApplicationUser user = default!;
+ private IEnumerable? recoveryCodes;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ protected override async Task OnInitializedAsync()
+ {
+ user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+
+ var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
+ if (!isTwoFactorEnabled)
+ {
+ throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled.");
+ }
+ }
+
+ private async Task OnSubmitAsync()
+ {
+ var userId = await UserManager.GetUserIdAsync(user);
+ recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
+ message = "You have generated new recovery codes.";
+
+ Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/Index.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/Index.razor
new file mode 100644
index 0000000..ccae182
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/Index.razor
@@ -0,0 +1,77 @@
+@page "/Account/Manage"
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityUserAccessor UserAccessor
+@inject IdentityRedirectManager RedirectManager
+
+Profile
+
+Profile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save
+
+
+
+
+@code {
+ private ApplicationUser user = default!;
+ private string? username;
+ private string? phoneNumber;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+ username = await UserManager.GetUserNameAsync(user);
+ phoneNumber = await UserManager.GetPhoneNumberAsync(user);
+
+ Input.PhoneNumber ??= phoneNumber;
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ if (Input.PhoneNumber != phoneNumber)
+ {
+ var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
+ if (!setPhoneResult.Succeeded)
+ {
+ RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext);
+ }
+ }
+
+ await SignInManager.RefreshSignInAsync(user);
+ RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext);
+ }
+
+ private sealed class InputModel
+ {
+ [Phone]
+ [Display(Name = "Phone number")]
+ public string? PhoneNumber { get; set; }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/PersonalData.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/PersonalData.razor
new file mode 100644
index 0000000..5f3c009
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/PersonalData.razor
@@ -0,0 +1,41 @@
+@page "/Account/Manage/PersonalData"
+
+@inject IdentityUserAccessor UserAccessor
+
+Personal Data
+
+Personal data
+
+
+
+
+
+
+ Your account contains personal data that you have given us. This page allows you to download or delete that data.
+
+
+
+
+ Deleting this data will permanently remove your account, and this cannot be recovered.
+
+
+
+
+
+
+ Delete
+
+
+
+@code {
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ protected override async Task OnInitializedAsync()
+ {
+ _ = await UserAccessor.GetRequiredUserAsync(HttpContext);
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ResetAuthenticator.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ResetAuthenticator.razor
new file mode 100644
index 0000000..bc8eb7e
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/ResetAuthenticator.razor
@@ -0,0 +1,52 @@
+@page "/Account/Manage/ResetAuthenticator"
+
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityUserAccessor UserAccessor
+@inject IdentityRedirectManager RedirectManager
+@inject ILogger Logger
+
+Reset authenticator key
+
+Reset authenticator key
+
+
+
+
+ If you reset your authenticator key your authenticator app will not work until you reconfigure it.
+
+
+
+ This process disables 2FA until you verify your authenticator app.
+ If you do not complete your authenticator app configuration you may lose access to your account.
+
+
+
+
+@code {
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ private async Task OnSubmitAsync()
+ {
+ var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+ await UserManager.SetTwoFactorEnabledAsync(user, false);
+ await UserManager.ResetAuthenticatorKeyAsync(user);
+ var userId = await UserManager.GetUserIdAsync(user);
+ Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId);
+
+ await SignInManager.RefreshSignInAsync(user);
+
+ RedirectManager.RedirectToWithStatus(
+ "Account/Manage/EnableAuthenticator",
+ "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.",
+ HttpContext);
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/SetPassword.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/SetPassword.razor
new file mode 100644
index 0000000..5453106
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/SetPassword.razor
@@ -0,0 +1,87 @@
+@page "/Account/Manage/SetPassword"
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityUserAccessor UserAccessor
+@inject IdentityRedirectManager RedirectManager
+
+Set password
+
+
Set your password
+
+
+ You do not have a local username/password for this site. Add a local
+ account so you can log in without an external login.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ private string? message;
+ private ApplicationUser user = default!;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+
+ var hasPassword = await UserManager.HasPasswordAsync(user);
+ if (hasPassword)
+ {
+ RedirectManager.RedirectTo("Account/Manage/ChangePassword");
+ }
+ }
+
+ private async Task OnValidSubmitAsync()
+ {
+ var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!);
+ if (!addPasswordResult.Succeeded)
+ {
+ message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}";
+ return;
+ }
+
+ await SignInManager.RefreshSignInAsync(user);
+ RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext);
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ [Display(Name = "New password")]
+ public string? NewPassword { get; set; }
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm new password")]
+ [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
+ public string? ConfirmPassword { get; set; }
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/TwoFactorAuthentication.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/TwoFactorAuthentication.razor
new file mode 100644
index 0000000..8351ca3
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/TwoFactorAuthentication.razor
@@ -0,0 +1,110 @@
+@page "/Account/Manage/TwoFactorAuthentication"
+
+@using Microsoft.AspNetCore.Http.Features
+@using Microsoft.AspNetCore.Identity
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject SignInManager SignInManager
+@inject IdentityUserAccessor UserAccessor
+@inject IdentityRedirectManager RedirectManager
+
+Two-factor authentication (2FA)
+
+Two-factor authentication (2FA)
+
+
+
+@if (canTrack)
+{
+ if (is2faEnabled)
+ {
+ if (recoveryCodesLeft == 0)
+ {
+ You have no recovery codes left.
+
+
+ You must generate a new set of recovery codes
+ before you can log in with a recovery code.
+
+ }
+ else if (recoveryCodesLeft == 1)
+ {
+ You have 1 recovery code left.
+
+
+ You can generate a new set of recovery codes.
+
+ }
+ else if (recoveryCodesLeft <= 3)
+ {
+ You have @recoveryCodesLeft recovery codes left.
+
+
+ You should generate a new set of recovery codes.
+
+ }
+
+ if (isMachineRemembered)
+ {
+
+ }
+
+ Disable 2FA
+ Reset recovery codes
+ }
+
+ Authenticator app
+
+ @if (!hasAuthenticator)
+ {
+ Add authenticator app
+ }
+ else
+ {
+ Set up authenticator app
+ Reset authenticator app
+ }
+}
+else
+{
+ Privacy and cookie policy have not been accepted.
+
+
+ You must accept the policy before you can enable two factor authentication.
+
+}
+
+@code {
+ private bool canTrack;
+ private bool hasAuthenticator;
+ private int recoveryCodesLeft;
+ private bool is2faEnabled;
+ private bool isMachineRemembered;
+
+ [CascadingParameter]
+ private HttpContext HttpContext { get; set; } = default!;
+
+ protected override async Task OnInitializedAsync()
+ {
+ var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
+ canTrack = HttpContext.Features.Get()?.CanTrack ?? true;
+ hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null;
+ is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
+ isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user);
+ recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user);
+ }
+
+ private async Task OnSubmitForgetBrowserAsync()
+ {
+ await SignInManager.ForgetTwoFactorClientAsync();
+
+ RedirectManager.RedirectToCurrentPageWithStatus(
+ "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.",
+ HttpContext);
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/_Imports.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/_Imports.razor
new file mode 100644
index 0000000..ada5bb0
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Manage/_Imports.razor
@@ -0,0 +1,2 @@
+@layout ManageLayout
+@attribute [Microsoft.AspNetCore.Authorization.Authorize]
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Register.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Register.razor
new file mode 100644
index 0000000..4e6a001
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/Register.razor
@@ -0,0 +1,146 @@
+@page "/Account/Register"
+
+@using System.ComponentModel.DataAnnotations
+@using System.Text
+@using System.Text.Encodings.Web
+@using Microsoft.AspNetCore.Identity
+@using Microsoft.AspNetCore.WebUtilities
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject IUserStore UserStore
+@inject SignInManager SignInManager
+@inject IEmailSender EmailSender
+@inject ILogger Logger
+@inject NavigationManager NavigationManager
+@inject IdentityRedirectManager RedirectManager
+
+Register
+
+Register
+
+
+
+
+
+
+
+ Create a new account.
+
+
+
+
+
+
+
+
+
+
+
+
+ Register
+
+
+
+
+
+ Use another service to register.
+
+
+
+
+@code {
+ private IEnumerable? identityErrors;
+
+ [SupplyParameterFromForm]
+ private InputModel Input { get; set; } = new();
+
+ [SupplyParameterFromQuery]
+ private string? ReturnUrl { get; set; }
+
+ private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}";
+
+ public async Task RegisterUser(EditContext editContext)
+ {
+ var user = CreateUser();
+
+ await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
+ var emailStore = GetEmailStore();
+ await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
+ var result = await UserManager.CreateAsync(user, Input.Password);
+
+ if (!result.Succeeded)
+ {
+ identityErrors = result.Errors;
+ return;
+ }
+
+ Logger.LogInformation("User created a new account with password.");
+
+ var userId = await UserManager.GetUserIdAsync(user);
+ var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = NavigationManager.GetUriWithQueryParameters(
+ NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
+ new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl });
+
+ await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
+
+ if (UserManager.Options.SignIn.RequireConfirmedAccount)
+ {
+ RedirectManager.RedirectTo(
+ "Account/RegisterConfirmation",
+ new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl });
+ }
+
+ await SignInManager.SignInAsync(user, isPersistent: false);
+ RedirectManager.RedirectTo(ReturnUrl);
+ }
+
+ private static ApplicationUser CreateUser()
+ {
+ try
+ {
+ return Activator.CreateInstance();
+ }
+ catch
+ {
+ throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
+ $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor.");
+ }
+ }
+
+ private IUserEmailStore GetEmailStore()
+ {
+ if (!UserManager.SupportsUserEmail)
+ {
+ throw new NotSupportedException("The default UI requires a user store with email support.");
+ }
+ return (IUserEmailStore)UserStore;
+ }
+
+ private sealed class InputModel
+ {
+ [Required]
+ [EmailAddress]
+ [Display(Name = "Email")]
+ public string Email { get; set; } = "";
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ [Display(Name = "Password")]
+ public string Password { get; set; } = "";
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm password")]
+ [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
+ public string ConfirmPassword { get; set; } = "";
+ }
+}
diff --git a/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/RegisterConfirmation.razor b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/RegisterConfirmation.razor
new file mode 100644
index 0000000..dde3906
--- /dev/null
+++ b/IntegryControlPanel/IntegryControlPanel/Components/Account/Pages/RegisterConfirmation.razor
@@ -0,0 +1,68 @@
+@page "/Account/RegisterConfirmation"
+
+@using System.Text
+@using Microsoft.AspNetCore.Identity
+@using Microsoft.AspNetCore.WebUtilities
+@using IntegryControlPanel.Data
+
+@inject UserManager UserManager
+@inject IEmailSender EmailSender
+@inject NavigationManager NavigationManager
+@inject IdentityRedirectManager RedirectManager
+
+Register confirmation
+
+
Register confirmation
+
+
+
+@if (emailConfirmationLink is not null)
+{
+
+ This app does not currently have a real email sender registered, see these docs for how to configure a real email sender.
+ Normally this would be emailed: Click here to confirm your account
+
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ The Development environment shouldn't be enabled for deployed applications.
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
+ and restarting the app.
+