diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 59da349ee8b7..15904c95822f 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -28,7 +28,7 @@ Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relative Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState? *REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary! routeValues) -> void *REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary! -Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void +Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode? renderMode) -> void Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddNamedEvent(string! eventType, string! assignedName) -> void Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags.HasCallerSpecifiedRenderMode = 1 -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags diff --git a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs index 0799c0ffb18b..9654f83720ef 100644 --- a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs +++ b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs @@ -627,9 +627,12 @@ public void AddComponentReferenceCapture(int sequence, Action componentR /// Adds a frame indicating the render mode on the enclosing component frame. /// /// The . - public void AddComponentRenderMode(IComponentRenderMode renderMode) + public void AddComponentRenderMode(IComponentRenderMode? renderMode) { - ArgumentNullException.ThrowIfNull(renderMode); + if (renderMode is null) + { + return; + } // Note that a ComponentRenderMode frame is technically a child of the Component frame to which it applies, // hence the terminology of "adding" it rather than "setting" it. For performance reasons, the diffing system diff --git a/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs index 5e4729c740d8..1b07718c88c0 100644 --- a/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs +++ b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs @@ -2141,18 +2141,26 @@ public void CannotAddComponentRenderModeToElement() } [Fact] - public void CannotAddNullComponentRenderMode() + public void CanAddNullComponentRenderMode() { // Arrange var builder = new RenderTreeBuilder(); + + // Act builder.OpenComponent(0); + builder.AddComponentParameter(1, "param", 123); + builder.AddComponentRenderMode(null); + builder.CloseComponent(); - // Act/Assert - var ex = Assert.Throws(() => - { - builder.AddComponentRenderMode(null); - }); - Assert.Equal("renderMode", ex.ParamName); + // Assert + Assert.Collection( + builder.GetFrames().AsEnumerable(), + frame => + { + AssertFrame.Component(frame, 2, 0); + Assert.False(frame.ComponentFrameFlags.HasFlag(ComponentFrameFlags.HasCallerSpecifiedRenderMode)); + }, + frame => AssertFrame.Attribute(frame, "param", 123, 1)); } [Fact] diff --git a/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs b/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs index c5c1c3d21e1f..5df713d111c7 100644 --- a/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs +++ b/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs @@ -20,7 +20,8 @@ internal void SetRequestContext(HttpContext context) { if (_context == null) { - return null; + // We're in an interactive context. Use the token persisted during static rendering. + return base.GetAntiforgeryToken(); } // We already have a callback setup to generate the token when the response starts if needed. diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index 1712f7383a21..6a3d926a73a2 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -26,7 +26,7 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) { state.PersistAsJson(PersistenceKey, GetAntiforgeryToken()); return Task.CompletedTask; - }, RenderMode.InteractiveWebAssembly); + }, RenderMode.InteractiveAuto); state.TryTakeFromJson(PersistenceKey, out _currentToken); } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index a8a905e903a2..2a4596d54950 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs @@ -919,6 +919,19 @@ public void CanUseAntiforgeryTokenInWasm() DispatchToFormCore(dispatchToForm); } + [Fact] + public void CanUseAntiforgeryTokenWithServerInteractivity() + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/antiforgery-server-interactive", + FormCssSelector = "form", + InputFieldId = "value", + SuppressEnhancedNavigation = true, + }; + DispatchToFormCore(dispatchToForm); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 1c9960d2c88b..a718b124677f 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -8,7 +8,6 @@ using Components.TestServer.RazorComponents; using Components.TestServer.RazorComponents.Pages.Forms; using Components.TestServer.Services; -using Microsoft.AspNetCore.Components.WebAssembly.Server; using Microsoft.AspNetCore.Mvc; namespace TestServer; @@ -155,9 +154,18 @@ private static void MapEnhancedNavigationEndpoints(IEndpointRouteBuilder endpoin await response.WriteAsync("

This is a non-Blazor endpoint

That's all

"); }); - endpoints.MapPost("api/antiforgery-form", ([FromForm] string value) => + endpoints.MapPost("api/antiforgery-form", ( + [FromForm] string value, + [FromForm(Name = "__RequestVerificationToken")] string? inFormCsrfToken, + [FromHeader(Name = "RequestVerificationToken")] string? inHeaderCsrfToken) => { - return Results.Ok(value); + // We shouldn't get this far without a valid CSRF token, but we'll double check it's there. + if (string.IsNullOrEmpty(inFormCsrfToken) && string.IsNullOrEmpty(inHeaderCsrfToken)) + { + throw new InvalidOperationException("Invalid POST to api/antiforgery-form!"); + } + + return TypedResults.Text($"

Hello {value}!

", "text/html"); }); endpoints.Map("/forms/endpoint-that-never-finishes-rendering", (HttpResponse response, CancellationToken token) => diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/FormRenderedWithServerInteractivityCanUseAntiforgeryToken.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/FormRenderedWithServerInteractivityCanUseAntiforgeryToken.razor new file mode 100644 index 000000000000..f96cc826c389 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/FormRenderedWithServerInteractivityCanUseAntiforgeryToken.razor @@ -0,0 +1,21 @@ +@page "/forms/antiforgery-server-interactive" + +@using Microsoft.AspNetCore.Components.Forms + +@rendermode RenderMode.InteractiveServer + +

FormRenderedWithServerInteractivityCanUseAntiforgeryToken

+ +
+ + + @if (HttpContext is null) + { + + } + + +@code { + [CascadingParameter] + HttpContext? HttpContext { get; set; } +} diff --git a/src/Components/test/testassets/TestContentPackage/WasmFormComponent.razor b/src/Components/test/testassets/TestContentPackage/WasmFormComponent.razor index 305d6efd04dc..0edd4b98c726 100644 --- a/src/Components/test/testassets/TestContentPackage/WasmFormComponent.razor +++ b/src/Components/test/testassets/TestContentPackage/WasmFormComponent.razor @@ -16,7 +16,7 @@ { @if (_succeeded) { -

Posting the value succeded.

+

Posting the value succeeded.

} else { @@ -42,7 +42,7 @@ else if (OperatingSystem.IsBrowser()) { var antiforgery = AntiforgeryState.GetAntiforgeryToken(); - _token = antiforgery.Value; + _token = antiforgery.Value; } } diff --git a/src/ProjectTemplates/README-BASELINES.md b/src/ProjectTemplates/README-BASELINES.md new file mode 100644 index 000000000000..7268cd5eee1f --- /dev/null +++ b/src/ProjectTemplates/README-BASELINES.md @@ -0,0 +1,19 @@ +# Generating template-baselines.json + +For small project template changes, you may be able to edit the `template-baselines.json` file manually. This is a good way to ensure you have correct expectations about the effects of your changes. + +For larger changes such as adding entirely new templates, it may be impractical to type out the changes to `template-baselines.json` manually. In those cases you can follow a procedure like the following. + + 1. Ensure you've configured the necessary environment variables: + - `set PATH=c:\git\dotnet\aspnetcore\.dotnet\;%PATH%` (update path as needed) + - `set DOTNET_ROOT=c:\git\dotnet\aspnetcore\.dotnet` (update path as needed) + 2. Get to a position where you can execute the modified template(s) locally, i.e.: + - Use `dotnet pack ProjectTemplatesNoDeps.slnf` (possibly with `--no-restore --no-dependencies`) to regenerate `Microsoft.DotNet.Web.ProjectTemplates.*.nupkg` + - Run one of the `scripts/*.ps1` scripts to install your template pack and execute your chosen template. For example, run `powershell .\scripts\Run-BlazorWeb-Locally.ps1` + - Once that has run, you should see your updated template listed when you execute `dotnet new list` or `dotnet new YourTemplateName --help`. At the point you can run `dotnet new YourTemplateName -o SomePath` directly if you want. However each time you edit template sources further, you will need to run `dotnet new uninstall Microsoft.DotNet.Web.ProjectTemplates.8.0` and then go back to the start of this whole step. + - Tip: the following command combines the above steps, to go directly from editing template sources to an updated local project output: `dotnet pack ProjectTemplatesNoDeps.slnf --no-restore --no-dependencies && dotnet new uninstall Microsoft.DotNet.Web.ProjectTemplates.8.0 && rm -rf scripts\MyBlazorApp && powershell .\scripts\Run-BlazorWeb-Locally.ps1` + 3. After generating a particular project's output, the following can be run in a Bash prompt (e.g., using WSL): + - `cd src/ProjectTemplates/scripts` + - `export PROJECT_NAME=MyBlazorApp` (update as necessary - note this is the name of the directly under `scripts` containing your project output) + - `find $PROJECT_NAME -type f -not -path "*/obj/*" -not -path "*/bin/*" -not -path "*/.publish/*" | sed -e "s/^$PROJECT_NAME\///" | sed -e "s/$PROJECT_NAME/{ProjectName}/g" | sed 's/.*/ "&",/' | sort -f` + - This will emit the JSON-formatted lines you can manually insert into the relevant place inside `template-baselines.json` diff --git a/src/ProjectTemplates/README.md b/src/ProjectTemplates/README.md index 9ccbd3e87185..6a4287900558 100644 --- a/src/ProjectTemplates/README.md +++ b/src/ProjectTemplates/README.md @@ -50,7 +50,7 @@ Otherwise, you'll get a test error "Certificate error: Navigation blocked". Then, use one of: -1. Run `src\ProjectTemplates\build.cmd -test -NoRestore -NoBuild -NoBuilddeps -configuration Release` (or equivalent src\ProjectTemplates\build.sh` command) to run all template tests. +1. Run `src\ProjectTemplates\build.cmd -test -NoRestore -NoBuild -NoBuildDeps -configuration Release` (or equivalent src\ProjectTemplates\build.sh` command) to run all template tests. 1. To test specific templates, use the `Run-[Template]-Locally.ps1` scripts in the script folder. - These scripts do `dotnet new -i` with your packages, but also apply a series of fixes and tweaks to the created template which keep the fact that you don't have a production `Microsoft.AspNetCore.App` from interfering. 1. Run templates manually with `custom-hive` and `disable-sdk-templates` to install to a custom location and turn off the built-in templates e.g. diff --git a/src/ProjectTemplates/Shared/ArgConstants.cs b/src/ProjectTemplates/Shared/ArgConstants.cs index 342f8ce80c48..eafa09fe6f86 100644 --- a/src/ProjectTemplates/Shared/ArgConstants.cs +++ b/src/ProjectTemplates/Shared/ArgConstants.cs @@ -14,7 +14,6 @@ internal static class ArgConstants public const string CalledApiScopes = "--called-api-scopes"; public const string CalledApiScopesUserReadWrite = $"{CalledApiScopes} user.readwrite"; public const string NoOpenApi = "--no-openapi"; - public const string Auth = "-au"; public const string ClientId = "--client-id"; public const string Domain = "--domain"; public const string DefaultScope = "--default-scope"; @@ -26,5 +25,9 @@ internal static class ArgConstants public const string NoHttps = "--no-https"; public const string PublishNativeAot = "--aot"; public const string NoInteractivity = "--interactivity none"; + public const string WebAssemblyInteractivity = "--interactivity WebAssembly"; + public const string AutoInteractivity = "--interactivity Auto"; + public const string GlobalInteractivity = "--all-interactive"; public const string Empty = "--empty"; + public const string IndividualAuth = "--auth Individual"; } diff --git a/src/ProjectTemplates/Shared/AspNetProcess.cs b/src/ProjectTemplates/Shared/AspNetProcess.cs index cc1b9400f406..cf2205bbd5b5 100644 --- a/src/ProjectTemplates/Shared/AspNetProcess.cs +++ b/src/ProjectTemplates/Shared/AspNetProcess.cs @@ -43,9 +43,6 @@ public AspNetProcess( _output = output; _httpClient = new HttpClient(new HttpClientHandler() { - AllowAutoRedirect = true, - UseCookies = true, - CookieContainer = new CookieContainer(), ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => (certificate.Subject != "CN=localhost" && errors == SslPolicyErrors.None) || certificate?.Thumbprint == _developmentCertificate.CertificateThumbprint, }) { @@ -124,6 +121,14 @@ public async Task AssertPagesOk(IEnumerable pages) } } + public async Task AssertPagesNotFound(IEnumerable urls) + { + foreach (var url in urls) + { + await AssertNotFound(url); + } + } + public async Task ContainsLinks(Page page) { var response = await RetryHelper.RetryRequest(async () => @@ -290,8 +295,10 @@ public override string ToString() } } -public class Page +public class Page(string url) { - public string Url { get; set; } + public Page() : this(null) { } + + public string Url { get; set; } = url; public IEnumerable Links { get; set; } } diff --git a/src/ProjectTemplates/TestInfrastructure/PrepareForTest.targets b/src/ProjectTemplates/TestInfrastructure/PrepareForTest.targets index 63a85fe564fa..156b26ffb9cb 100644 --- a/src/ProjectTemplates/TestInfrastructure/PrepareForTest.targets +++ b/src/ProjectTemplates/TestInfrastructure/PrepareForTest.targets @@ -6,7 +6,7 @@ <_Parameter1>DotNetEfFullPath - <_Parameter2>$([MSBuild]::EnsureTrailingSlash('$(NuGetPackageRoot)'))dotnet-ef/$(DotnetEfVersion)/tools/net6.0/any/dotnet-ef.dll + <_Parameter2>$([MSBuild]::EnsureTrailingSlash('$(NuGetPackageRoot)'))dotnet-ef/$(DotnetEfVersion)/tools/net8.0/any/dotnet-ef.dll <_Parameter1>TestPackageRestorePath diff --git a/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in index d676d90df723..541be08bd263 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in @@ -12,7 +12,7 @@ - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json index 90bd34266178..607c7fa41e68 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json @@ -54,8 +54,6 @@ { "condition": "(!UseWebAssembly)", "exclude": [ - "BlazorWeb-CSharp/wwwroot/appsettings.Development.json", - "BlazorWeb-CSharp/wwwroot/appsettings.json", "BlazorWeb-CSharp.Client/**", "*.sln" ], @@ -67,7 +65,8 @@ "condition": "(UseWebAssembly && InteractiveAtRoot)", "rename": { "BlazorWeb-CSharp/Components/Layout/": "./BlazorWeb-CSharp.Client/Layout/", - "BlazorWeb-CSharp/Components/Pages/": "./BlazorWeb-CSharp.Client/Pages/", + "BlazorWeb-CSharp/Components/Pages/Home.razor": "./BlazorWeb-CSharp.Client/Pages/Home.razor", + "BlazorWeb-CSharp/Components/Pages/Weather.razor": "./BlazorWeb-CSharp.Client/Pages/Weather.razor", "BlazorWeb-CSharp/Components/Routes.razor": "./BlazorWeb-CSharp.Client/Routes.razor" } }, @@ -101,6 +100,7 @@ { "condition": "(!SampleContent)", "exclude": [ + "BlazorWeb-CSharp/Components/Pages/Auth.*", "BlazorWeb-CSharp/Components/Pages/Counter.*", "BlazorWeb-CSharp/Components/Pages/Weather.*", "BlazorWeb-CSharp/Components/Layout/NavMenu.*", @@ -113,12 +113,8 @@ { "condition": "(!IndividualLocalAuth)", "exclude": [ - "BlazorWeb-CSharp/Components/Identity/**", - "BlazorWeb-CSharp/Components/Layout/ManageLayout.razor", - "BlazorWeb-CSharp/Components/Layout/ManageNavMenu.razor", - "BlazorWeb-CSharp/Components/Pages/Account/**", + "BlazorWeb-CSharp/Components/Account/**", "BlazorWeb-CSharp/Data/**", - "BlazorWeb-CSharp/Identity/**", "BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs", "BlazorWeb-CSharp.Client/UserInfo.cs", "BlazorWeb-CSharp.Client/Pages/Auth.razor" @@ -127,7 +123,7 @@ { "condition": "(!(IndividualLocalAuth && !UseLocalDB))", "exclude": [ - "BlazorWeb-CSharp/app.db" + "BlazorWeb-CSharp/Data/app.db" ] }, { @@ -139,19 +135,19 @@ { "condition": "(!(IndividualLocalAuth && UseServer && UseWebAssembly))", "exclude": [ - "BlazorWeb-CSharp/Identity/PersistingRevalidatingAuthenticationStateProvider.cs" + "BlazorWeb-CSharp/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs" ] }, { "condition": "(!(IndividualLocalAuth && UseServer && !UseWebAssembly))", "exclude": [ - "BlazorWeb-CSharp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs" + "BlazorWeb-CSharp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs" ] }, { "condition": "(!(IndividualLocalAuth && !UseServer && UseWebAssembly))", "exclude": [ - "BlazorWeb-CSharp/Identity/PersistingServerAuthenticationStateProvider.cs" + "BlazorWeb-CSharp/Components/Account/PersistingServerAuthenticationStateProvider.cs" ] }, { @@ -189,6 +185,12 @@ "exclude": [ "BlazorWeb-CSharp/Data/SqlServer/**" ] + }, + { + "condition": "(IndividualLocalAuth && UseWebAssembly)", + "rename": { + "BlazorWeb-CSharp/Components/Account/Shared/RedirectToLogin.razor": "BlazorWeb-CSharp.Client/RedirectToLogin.razor" + } } ] } @@ -250,7 +252,7 @@ "sourceVariableName": "kestrelHttpPort", "fallbackVariableName": "kestrelHttpPortGenerated" }, - "replaces": "5000" + "replaces": "5500" }, "kestrelHttpsPort": { "type": "parameter", @@ -272,7 +274,7 @@ "sourceVariableName": "kestrelHttpsPort", "fallbackVariableName": "kestrelHttpsPortGenerated" }, - "replaces": "5001" + "replaces": "5501" }, "iisHttpPort": { "type": "parameter", @@ -349,7 +351,7 @@ "defaultValue": "InteractivePerPage", "displayName": "_Interactivity location", "description": "Chooses which components will have interactive rendering enabled", - "isEnabled": "(InteractivityPlatform != \"None\" && auth == \"None\")", + "isEnabled": "(InteractivityPlatform != \"None\")", "choices": [ { "choice": "InteractivePerPage", @@ -413,7 +415,7 @@ "AllInteractive": { "type": "parameter", "datatype": "bool", - "isEnabled": "(InteractivityPlatform != \"None\" && auth == \"None\")", + "isEnabled": "(InteractivityPlatform != \"None\")", "defaultValue": "false", "displayName": "_Enable interactive rendering globally throughout the site", "description": "Configures whether to make every page interactive by applying an interactive render mode at the top level. If false, pages will use static server rendering by default, and can be marked interactive on a per-page or per-component basis." diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs index 4f8e698ea75d..a02de48944aa 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs @@ -4,16 +4,24 @@ namespace BlazorWeb_CSharp.Client; +// This is a client-side AuthenticationStateProvider that determines the user's authentication state by +// looking for data persisted in the page when it was rendered on the server. This authentication state will +// be fixed for the lifetime of the WebAssembly application. So, if the user needs to log in or out, a full +// page reload is required. +// +// This only provides a user name and email for display purposes. It does not actually include any tokens +// that authenticate to the server when making subsequent requests. That works separately using a +// cookie that will be included on HttpClient requests to the server. public class PersistentAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider { - private static readonly Task _unauthenticatedTask = + private static readonly Task unauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); public override Task GetAuthenticationStateAsync() { if (!persistentState.TryTakeFromJson(nameof(UserInfo), out var userInfo) || userInfo is null) { - return _unauthenticatedTask; + return unauthenticatedTask; } Claim[] claims = [ @@ -26,4 +34,3 @@ public override Task GetAuthenticationStateAsync() authenticationType: nameof(PersistentAuthenticationStateProvider))))); } } - diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/UserInfo.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/UserInfo.cs index 236bbaa720da..b62cdbbe0c1b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/UserInfo.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/UserInfo.cs @@ -1,5 +1,7 @@ namespace BlazorWeb_CSharp.Client; +// Add properties to this class and update the server and client AuthenticationStateProviders +// to expose more information about the authenticated user to the client. public class UserInfo { public required string UserId { get; set; } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs similarity index 80% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs index ea968cb71f5e..630eaaf25cb9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -1,19 +1,20 @@ +using System.Security.Claims; using System.Text.Json; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using BlazorWeb_CSharp.Components.Pages.Account; -using BlazorWeb_CSharp.Components.Pages.Account.Manage; -using BlazorWeb_CSharp.Data; -using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Primitives; +using BlazorWeb_CSharp.Components.Account.Pages; +using BlazorWeb_CSharp.Components.Account.Pages.Manage; +using BlazorWeb_CSharp.Data; namespace Microsoft.AspNetCore.Routing; internal static class IdentityComponentsEndpointRouteBuilderExtensions { - // These endpoints are required by the Identity Razor components defined in the /Components/Pages/Account directory of this project. + // 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); @@ -32,11 +33,20 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn var redirectUrl = UriHelper.BuildRelative( context.Request.PathBase, - $"/Account/ExternalLogin", + "/Account/ExternalLogin", QueryString.Create(query)); var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); - return Results.Challenge(properties, [provider]); + return TypedResults.Challenge(properties, [provider]); + }); + + accountGroup.MapPost("/Logout", async ( + ClaimsPrincipal user, + SignInManager signInManager, + [FromForm] string returnUrl) => + { + await signInManager.SignOutAsync(); + return TypedResults.LocalRedirect($"~/{returnUrl}"); }); var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); @@ -51,11 +61,11 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn var redirectUrl = UriHelper.BuildRelative( context.Request.PathBase, - $"/Account/Manage/ExternalLogins", + "/Account/Manage/ExternalLogins", QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User)); - return Results.Challenge(properties, [provider]); + return TypedResults.Challenge(properties, [provider]); }); var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); @@ -90,11 +100,11 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); } - personalData.Add($"Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); + personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData); context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); - return Results.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); + return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); }); return accountGroup; diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityNoOpEmailSender.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityNoOpEmailSender.cs new file mode 100644 index 000000000000..1256a02a78ba --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityNoOpEmailSender.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using BlazorWeb_CSharp.Data; + +namespace BlazorWeb_CSharp.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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityRedirectManager.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityRedirectManager.cs new file mode 100644 index 000000000000..da85e6efd46d --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityRedirectManager.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; + +namespace BlazorWeb_CSharp.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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs similarity index 66% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs index 237200d2cae0..cf8bf6d8930b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs @@ -5,30 +5,23 @@ using Microsoft.Extensions.Options; using BlazorWeb_CSharp.Data; -namespace BlazorWeb_CSharp.Identity; +namespace BlazorWeb_CSharp.Components.Account; -public class IdentityRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider -{ - private readonly IServiceScopeFactory _scopeFactory; - private readonly IdentityOptions _options; - - public IdentityRevalidatingAuthenticationStateProvider( +// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user +// every 30 minutes an interactive circuit is connected. +internal sealed class IdentityRevalidatingAuthenticationStateProvider( ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory, - IOptions optionsAccessor) - : base(loggerFactory) - { - _scopeFactory = scopeFactory; - _options = optionsAccessor.Value; - } - + IOptions options) + : RevalidatingServerAuthenticationStateProvider(loggerFactory) +{ protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); protected override async Task ValidateAuthenticationStateAsync( AuthenticationState authenticationState, CancellationToken cancellationToken) { // Get the user manager from a new scope to ensure it fetches fresh data - await using var scope = _scopeFactory.CreateAsyncScope(); + await using var scope = scopeFactory.CreateAsyncScope(); var userManager = scope.ServiceProvider.GetRequiredService>(); return await ValidateSecurityStampAsync(userManager, authenticationState.User); } @@ -36,7 +29,7 @@ protected override async Task ValidateAuthenticationStateAsync( private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) { var user = await userManager.GetUserAsync(principal); - if (user == null) + if (user is null) { return false; } @@ -46,7 +39,7 @@ private async Task ValidateSecurityStampAsync(UserManager } else { - var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType); + var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType); var userStamp = await userManager.GetSecurityStampAsync(user); return principalStamp == userStamp; } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityUserAccessor.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityUserAccessor.cs new file mode 100644 index 000000000000..86e027c0b6ee --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityUserAccessor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; +using BlazorWeb_CSharp.Data; + +namespace BlazorWeb_CSharp.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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ConfirmEmail.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ConfirmEmail.razor new file mode 100644 index 000000000000..830ee204c824 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ConfirmEmail.razor @@ -0,0 +1,48 @@ +@page "/Account/ConfirmEmail" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using BlazorWeb_CSharp.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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmailChange.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ConfirmEmailChange.razor similarity index 63% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmailChange.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ConfirmEmailChange.razor index 510fb2d0e93f..478a810defc9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmailChange.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ConfirmEmailChange.razor @@ -4,22 +4,22 @@ @using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.WebUtilities @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager -@inject UserAccessor UserAccessor @inject IdentityRedirectManager RedirectManager Confirm email change

Confirm email change

- + @code { - private string? _message; - private ApplicationUser _user = default!; + private string? message; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromQuery] private string? UserId { get; set; } @@ -35,30 +35,34 @@ if (UserId is null || Email is null || Code is null) { RedirectManager.RedirectToWithStatus( - "/Account/Login", "Error: Invalid email change confirmation link."); - return; + "Account/Login", "Error: Invalid email change confirmation link.", HttpContext); } - _user = await UserAccessor.GetRequiredUserAsync(); + 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); + var result = await UserManager.ChangeEmailAsync(user, Email, code); if (!result.Succeeded) { - _message = "Error changing email."; + 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); + var setUserNameResult = await UserManager.SetUserNameAsync(user, Email); if (!setUserNameResult.Succeeded) { - _message = "Error changing user name."; + message = "Error changing user name."; return; } - await SignInManager.RefreshSignInAsync(_user); - _message = "Thank you for confirming your email change."; + await SignInManager.RefreshSignInAsync(user); + message = "Thank you for confirming your email change."; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ExternalLogin.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ExternalLogin.razor similarity index 57% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ExternalLogin.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ExternalLogin.razor index 51bfa81fc116..730744b216cf 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ExternalLogin.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ExternalLogin.razor @@ -5,43 +5,37 @@ @using System.Text @using System.Text.Encodings.Web @using Microsoft.AspNetCore.Identity -@using Microsoft.AspNetCore.Identity.UI.Services @using Microsoft.AspNetCore.WebUtilities @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject SignInManager SignInManager @inject UserManager UserManager @inject IUserStore UserStore -@inject IEmailSender EmailSender +@inject IEmailSender EmailSender @inject NavigationManager NavigationManager @inject IdentityRedirectManager RedirectManager @inject ILogger Logger -@{ - var providerDisplayName = _externalLoginInfo.ProviderDisplayName; -} - Register - +

Register

-

Associate your @providerDisplayName account.

+

Associate your @ProviderDisplayName account.


-
- You've successfully authenticated with @providerDisplayName. +
+ You've successfully authenticated with @ProviderDisplayName. Please enter an email address for this site below and click the Register button to finish logging in.
- + - +
- +
@@ -53,45 +47,40 @@ @code { public const string LoginCallbackAction = "LoginCallback"; - private string? _message; - private ExternalLoginInfo _externalLoginInfo = default!; - private IUserEmailStore _emailStore = default!; - - [SupplyParameterFromQuery] - private string? RemoteError { get; set; } + private string? message; + private ExternalLoginInfo externalLoginInfo = default!; [CascadingParameter] - public HttpContext HttpContext { get; set; } = default!; + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? RemoteError { get; set; } [SupplyParameterFromQuery] - private string ReturnUrl { get; set; } = default!; + private string? ReturnUrl { get; set; } [SupplyParameterFromQuery] - private string? Action { get; set; } = default!; + private string? Action { get; set; } + + private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName; protected override async Task OnInitializedAsync() { - Input ??= new(); - ReturnUrl ??= "/"; - if (RemoteError is not null) { - RedirectManager.RedirectToWithStatus("/Account/Login", "Error from external provider: " + RemoteError); - return; + RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext); } - var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); - if (externalLoginInfo is null) + var info = await SignInManager.GetExternalLoginInfoAsync(); + if (info is null) { - RedirectManager.RedirectToWithStatus("/Account/Login", "Error loading external login information."); - return; + RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext); } - _externalLoginInfo = externalLoginInfo; - _emailStore = GetEmailStore(); + externalLoginInfo = info; if (HttpMethods.IsGet(HttpContext.Request.Method)) { @@ -103,8 +92,7 @@ // 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"); - return; + RedirectManager.RedirectTo("Account/Login"); } } @@ -112,74 +100,68 @@ { // 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, + 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); + externalLoginInfo.Principal.Identity?.Name, + externalLoginInfo.LoginProvider); RedirectManager.RedirectTo(ReturnUrl); - return; } - - if (result.IsLockedOut) + else if (result.IsLockedOut) { - RedirectManager.RedirectTo("/Account/Lockout"); - return; + 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)) + if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) { - Input.Email = _externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email); + Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? ""; } } private async Task OnValidSubmitAsync() { + var emailStore = GetEmailStore(); var user = CreateUser(); await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); - await _emailStore.SetEmailAsync(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); + result = await UserManager.AddLoginAsync(user, externalLoginInfo); if (result.Succeeded) { - Logger.LogInformation("User created an account using {Name} provider.", _externalLoginInfo.LoginProvider); + 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.BaseUri}Account/ConfirmEmail", - new Dictionary { { "userId", userId }, { "code", code } }); - await EmailSender.SendEmailAsync(Input.Email!, "Confirm your email", - $"Please confirm your account by clicking here."); + 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 }); - return; + RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email }); } - await SignInManager.SignInAsync(user, isPersistent: false, _externalLoginInfo.LoginProvider); + await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider); RedirectManager.RedirectTo(ReturnUrl); - return; } } - else - { - _message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}"; - } + + message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}"; } private ApplicationUser CreateUser() @@ -208,6 +190,6 @@ { [Required] [EmailAddress] - public string? Email { get; set; } + public string Email { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ForgotPassword.razor similarity index 67% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPassword.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ForgotPassword.razor index 605a676daad5..63103e8eb4c5 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPassword.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ForgotPassword.razor @@ -4,15 +4,13 @@ @using System.Text @using System.Text.Encodings.Web @using Microsoft.AspNetCore.Identity -@using Microsoft.AspNetCore.Identity.UI.Services @using Microsoft.AspNetCore.WebUtilities @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity +@inject UserManager UserManager +@inject IEmailSender EmailSender @inject NavigationManager NavigationManager @inject IdentityRedirectManager RedirectManager -@inject UserManager UserManager -@inject IEmailSender EmailSender Forgot your password? @@ -23,10 +21,10 @@
- +
- +
@@ -45,8 +43,7 @@ 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"); - return; + RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); } // For more information on how to enable account confirmation and password reset please @@ -54,21 +51,18 @@ var code = await UserManager.GeneratePasswordResetTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = NavigationManager.GetUriWithQueryParameters( - $"{NavigationManager.BaseUri}Account/ResetPassword", - new Dictionary { { "code", code } }); + NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri, + new Dictionary { ["code"] = code }); - await EmailSender.SendEmailAsync( - Input.Email, - "Reset Password", - $"Please reset your password by clicking here."); + await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); - RedirectManager.RedirectTo("/Account/ForgotPasswordConfirmation"); + RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); } private sealed class InputModel { [Required] [EmailAddress] - public string Email { get; set; } = default!; + public string Email { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPasswordConfirmation.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ForgotPasswordConfirmation.razor similarity index 100% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPasswordConfirmation.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ForgotPasswordConfirmation.razor diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidPasswordReset.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/InvalidPasswordReset.razor similarity index 100% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidPasswordReset.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/InvalidPasswordReset.razor diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidUser.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/InvalidUser.razor similarity index 100% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidUser.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/InvalidUser.razor diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Lockout.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Lockout.razor similarity index 100% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Lockout.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Lockout.razor diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Login.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Login.razor similarity index 67% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Login.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Login.razor index c0b708a91a13..0fd7e5cda415 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Login.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Login.razor @@ -1,12 +1,10 @@ @page "/Account/Login" @using System.ComponentModel.DataAnnotations -@using System.Text @using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.WebUtilities @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject SignInManager SignInManager @inject ILogger Logger @@ -20,18 +18,18 @@
- +

Use a local account to log in.


- +
- +
@@ -42,17 +40,17 @@
- +
@@ -68,36 +66,19 @@
@code { - string? errorMessage; + private string? errorMessage; [CascadingParameter] - public HttpContext HttpContext { get; set; } = default!; + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - public InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); [SupplyParameterFromQuery] - public string ReturnUrl { get; set; } = ""; - - public class InputModel - { - [Required] - [EmailAddress] - public string Email { get; set; } = null!; - - [Required] - [DataType(DataType.Password)] - public string Password { get; set; } = null!; - - [Display(Name = "Remember me?")] - public bool RememberMe { get; set; } = false; - } + private string? ReturnUrl { get; set; } protected override async Task OnInitializedAsync() { - Input ??= new(); - ReturnUrl ??= "/"; - if (HttpMethods.IsGet(HttpContext.Request.Method)) { // Clear the existing external cookie to ensure a clean login process @@ -115,20 +96,34 @@ Logger.LogInformation("User logged in."); RedirectManager.RedirectTo(ReturnUrl); } - if (result.RequiresTwoFactor) + else if (result.RequiresTwoFactor) { RedirectManager.RedirectTo( - "/Account/LoginWith2fa", - new() { ["ReturnUrl"] = ReturnUrl, ["RememberMe"] = Input.RememberMe }); + "Account/LoginWith2fa", + new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); } - if (result.IsLockedOut) + else if (result.IsLockedOut) { Logger.LogWarning("User account locked out."); - RedirectManager.RedirectTo("/Account/Lockout"); + 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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWith2fa.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/LoginWith2fa.razor similarity index 69% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWith2fa.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/LoginWith2fa.razor index b1a650544608..e522a248fee9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWith2fa.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/LoginWith2fa.razor @@ -3,7 +3,6 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject SignInManager SignInManager @inject UserManager UserManager @@ -14,7 +13,7 @@

Two-factor authentication


- +

Your login is protected with an authenticator app. Enter your authenticator code below.

@@ -22,15 +21,15 @@ - +
- +
@@ -42,15 +41,15 @@

Don't have access to your authenticator device? You can - log in with a recovery code. + log in with a recovery code.

@code { - private string? _message; - private ApplicationUser _user = default!; + private string? message; + private ApplicationUser user = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } @@ -60,38 +59,31 @@ protected override async Task OnInitializedAsync() { - Input ??= new(); - ReturnUrl ??= "/"; - - var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); - if (user is null) - { - throw new InvalidOperationException($"Unable to load two-factor authentication user."); - } - - _user = user; + // 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); + var userId = await UserManager.GetUserIdAsync(user); if (result.Succeeded) { - Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", _user.Id); - RedirectManager.RedirectTo(ReturnUrl ?? "/"); + 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.", _user.Id); - RedirectManager.RedirectTo("/Account/Lockout"); + 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}'.", _user.Id); - _message = "Error: Invalid authenticator code."; + Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId); + message = "Error: Invalid authenticator code."; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWithRecoveryCode.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/LoginWithRecoveryCode.razor similarity index 64% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWithRecoveryCode.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/LoginWithRecoveryCode.razor index 41d5d3660810..44c2981fc944 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWithRecoveryCode.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/LoginWithRecoveryCode.razor @@ -4,7 +4,6 @@ @using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Mvc @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject SignInManager SignInManager @inject UserManager UserManager @@ -15,7 +14,7 @@

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. @@ -24,9 +23,9 @@

- +
- +
@@ -36,51 +35,44 @@
@code { - private string? _message; - private ApplicationUser _user = default!; + private string? message; + private ApplicationUser user = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } protected override async Task OnInitializedAsync() { - Input ??= new(); - // Ensure the user has gone through the username & password screen first - var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); - if (user is null) - { - throw new InvalidOperationException($"Unable to load two-factor authentication user."); - } - - _user = user; + 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 recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); - var userId = await UserManager.GetUserIdAsync(_user); + var userId = await UserManager.GetUserIdAsync(user); if (result.Succeeded) { - Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", _user.Id); - RedirectManager.RedirectTo(ReturnUrl ?? "/"); + Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId); + RedirectManager.RedirectTo(ReturnUrl); } - if (result.IsLockedOut) + else if (result.IsLockedOut) { Logger.LogWarning("User account locked out."); - RedirectManager.RedirectTo("/Account/Lockout"); + RedirectManager.RedirectTo("Account/Lockout"); } else { - Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", _user.Id); - _message = "Error: Invalid recovery code entered."; + Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId); + message = "Error: Invalid recovery code entered."; } } @@ -89,6 +81,6 @@ [Required] [DataType(DataType.Text)] [Display(Name = "Recovery Code")] - public string? RecoveryCode { get; set; } + public string RecoveryCode { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ChangePassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ChangePassword.razor similarity index 57% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ChangePassword.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ChangePassword.razor index aabf71983a62..6c5cc814431b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ChangePassword.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ChangePassword.razor @@ -3,35 +3,34 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor @inject IdentityRedirectManager RedirectManager @inject ILogger Logger Change password

Change password

- +
- + - +
- +
- +
- +
@@ -41,39 +40,39 @@
@code { - private string? _message; - private ApplicationUser _user = default!; - private bool _hasPassword; + private string? message; + private ApplicationUser user = default!; + private bool hasPassword; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); protected override async Task OnInitializedAsync() { - Input ??= new(); - - _user = await UserAccessor.GetRequiredUserAsync(); - _hasPassword = await UserManager.HasPasswordAsync(_user); - if (!_hasPassword) + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + hasPassword = await UserManager.HasPasswordAsync(user); + if (!hasPassword) { - RedirectManager.RedirectTo("/Account/Manage/SetPassword"); - return; + RedirectManager.RedirectTo("Account/Manage/SetPassword"); } } private async Task OnValidSubmitAsync() { - var changePasswordResult = await UserManager.ChangePasswordAsync(_user, Input.OldPassword!, Input.NewPassword!); + var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); if (!changePasswordResult.Succeeded) { - _message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}"; + message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}"; return; } - await SignInManager.RefreshSignInAsync(_user); + await SignInManager.RefreshSignInAsync(user); Logger.LogInformation("User changed their password successfully."); - RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed"); + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext); } private sealed class InputModel @@ -81,17 +80,17 @@ [Required] [DataType(DataType.Password)] [Display(Name = "Current password")] - public string? OldPassword { get; set; } + 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; } + 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; } + public string ConfirmPassword { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/DeletePersonalData.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/DeletePersonalData.razor similarity index 55% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/DeletePersonalData.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/DeletePersonalData.razor index c3ed9c38635a..14f7688083a0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/DeletePersonalData.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/DeletePersonalData.razor @@ -3,17 +3,16 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor @inject IdentityRedirectManager RedirectManager @inject ILogger Logger Delete Personal Data - +

Delete Personal Data

@@ -24,13 +23,13 @@
- + - - @if (_requirePassword) + + @if (requirePassword) {
- +
@@ -40,38 +39,40 @@
@code { - private string? _message; - private ApplicationUser _user = default!; - private bool _requirePassword; + private string? message; + private ApplicationUser user = default!; + private bool requirePassword; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); protected override async Task OnInitializedAsync() { Input ??= new(); - - _user = await UserAccessor.GetRequiredUserAsync(); - _requirePassword = await UserManager.HasPasswordAsync(_user); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + requirePassword = await UserManager.HasPasswordAsync(user); } private async Task OnValidSubmitAsync() { - if (_requirePassword && !await UserManager.CheckPasswordAsync(_user, Input.Password!)) + if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password)) { - _message = "Error: Incorrect password."; + message = "Error: Incorrect password."; return; } - var result = await UserManager.DeleteAsync(_user); - var userId = await UserManager.GetUserIdAsync(_user); + var result = await UserManager.DeleteAsync(user); if (!result.Succeeded) { - throw new InvalidOperationException($"Unexpected error occurred deleting user."); + 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(); @@ -80,6 +81,6 @@ private sealed class InputModel { [DataType(DataType.Password)] - public string? Password { get; set; } + public string Password { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Disable2fa.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Disable2fa.razor similarity index 61% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Disable2fa.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Disable2fa.razor index 562b5ca26577..d3969f457c9c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Disable2fa.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Disable2fa.razor @@ -2,10 +2,9 @@ @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor @inject IdentityRedirectManager RedirectManager @inject ILogger Logger @@ -20,7 +19,7 @@

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. + used in an authenticator app you should reset your authenticator keys.

@@ -32,37 +31,34 @@
@code { - private ApplicationUser _user = default!; + private ApplicationUser user = default!; [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; protected override async Task OnInitializedAsync() { - _user = await UserAccessor.GetRequiredUserAsync(); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); - if (HttpMethods.IsGet(HttpContext.Request.Method)) + if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user)) { - if (!await UserManager.GetTwoFactorEnabledAsync(_user)) - { - throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled."); - } - return; + throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled."); } } private async Task OnSubmitAsync() { - var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(_user, false); + var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false); if (!disable2faResult.Succeeded) { - throw new InvalidOperationException($"Unexpected error occurred disabling 2FA."); + throw new InvalidOperationException("Unexpected error occurred disabling 2FA."); } - var userId = await UserManager.GetUserIdAsync(_user); + var userId = await UserManager.GetUserIdAsync(user); Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); RedirectManager.RedirectToWithStatus( - "/Account/Manage/TwoFactorAuthentication", - "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"); + "Account/Manage/TwoFactorAuthentication", + "2fa has been disabled. You can reenable 2fa when you setup an authenticator app", + HttpContext); } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Email.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Email.razor new file mode 100644 index 000000000000..0159af674e4a --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Email.razor @@ -0,0 +1,123 @@ +@page "/Account/Manage/Email" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using BlazorWeb_CSharp.Data + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject IdentityUserAccessor UserAccessor +@inject NavigationManager NavigationManager + +Manage email + +

Manage email

+ + +
+
+
+ + + + + + @if (isEmailConfirmed) + { +
+ +
+ +
+ +
+ } + else + { +
+ + + +
+ } +
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + private string? email; + private bool isEmailConfirmed; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm(FormName = "change-email")] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + email = await UserManager.GetEmailAsync(user); + isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user); + + Input.NewEmail ??= email; + } + + private async Task OnValidSubmitAsync() + { + if (Input.NewEmail is null || Input.NewEmail == email) + { + message = "Your email is unchanged."; + return; + } + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code }); + + await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Confirmation link to change email sent. Please check your email."; + } + + private async Task OnSendEmailVerificationAsync() + { + if (email is null) + { + return; + } + + 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, email, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Verification email sent. Please check your email."; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "New email")] + public string? NewEmail { get; set; } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/EnableAuthenticator.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/EnableAuthenticator.razor similarity index 68% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/EnableAuthenticator.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/EnableAuthenticator.razor index 52b3f0d68c5a..d0f0e161c7ff 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/EnableAuthenticator.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/EnableAuthenticator.razor @@ -6,23 +6,22 @@ @using System.Text.Encodings.Web @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor @inject UrlEncoder UrlEncoder @inject IdentityRedirectManager RedirectManager @inject ILogger Logger Configure authenticator app -@if (_recoveryCodes is not null) +@if (recoveryCodes is not null) { - + } else { - +

Configure authenticator app

To use an authenticator app go through the following steps:

@@ -38,10 +37,10 @@ else

  • -

    Scan the QR Code or enter this key @_sharedKey into your two factor authenticator app. Spaces and casing do not matter.

    +

    Scan the QR Code or enter this key @sharedKey into your two factor authenticator app. Spaces and casing do not matter.

    -
    -
    +
    +
  • @@ -50,15 +49,15 @@ else

    - +
    - +
    - +
    @@ -70,53 +69,52 @@ else @code { private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; - private ApplicationUser _user = default!; - private string? _sharedKey; - private string? _authenticatorUri; + private string? message; + private ApplicationUser user = default!; + private string? sharedKey; + private string? authenticatorUri; + private IEnumerable? recoveryCodes; - private IEnumerable? _recoveryCodes; - private string? _message; + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); protected override async Task OnInitializedAsync() { - Input ??= new(); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); - _user = await UserAccessor.GetRequiredUserAsync(); - - await LoadSharedKeyAndQrCodeUriAsync(_user); + await LoadSharedKeyAndQrCodeUriAsync(user); } private async Task OnValidSubmitAsync() { // Strip spaces and hyphens - var verificationCode = Input.Code!.Replace(" ", string.Empty).Replace("-", string.Empty); + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync( - _user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); if (!is2faTokenValid) { - await LoadSharedKeyAndQrCodeUriAsync(_user); - RedirectManager.RedirectToCurrentPageWithStatus("Error: Verification code is invalid."); + message = "Error: Verification code is invalid."; return; } - await UserManager.SetTwoFactorEnabledAsync(_user, true); - var userId = await UserManager.GetUserIdAsync(_user); + 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."; + message = "Your authenticator app has been verified."; - if (await UserManager.CountRecoveryCodesAsync(_user) == 0) + if (await UserManager.CountRecoveryCodesAsync(user) == 0) { - _recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(_user, 10); + recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); } else { - RedirectManager.RedirectToWithStatus("/Account/Manage/TwoFactorAuthentication", _message); + RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext); } } @@ -130,10 +128,10 @@ else unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); } - _sharedKey = FormatKey(unformattedKey!); + sharedKey = FormatKey(unformattedKey!); var email = await UserManager.GetEmailAsync(user); - _authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!); + authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!); } private string FormatKey(string unformattedKey) @@ -169,6 +167,6 @@ else [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; } + public string Code { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ExternalLogins.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ExternalLogins.razor similarity index 56% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ExternalLogins.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ExternalLogins.razor index 4ff1ec38044a..14e6117e3a5a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ExternalLogins.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ExternalLogins.razor @@ -4,30 +4,29 @@ @using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Mvc.ViewFeatures @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor @inject IUserStore UserStore @inject IdentityRedirectManager RedirectManager Manage your external logins -@if (_currentLogins?.Count > 0) +@if (currentLogins?.Count > 0) {

    Registered Logins

    - @foreach (var login in _currentLogins) + @foreach (var login in currentLogins) { - +
    @login.ProviderDisplayName@login.ProviderDisplayName - @if (_showRemoveButton) + @if (showRemoveButton) { -
    +
    @@ -46,23 +45,17 @@
    } -@if (_otherLogins?.Count > 0) +@if (otherLogins?.Count > 0) {

    Add another service to log in.


    - + -
    +

    - @foreach (var provider in _otherLogins) + @foreach (var provider in otherLogins) { - } @@ -74,10 +67,13 @@ @code { public const string LinkLoginCallbackAction = "LinkLoginCallback"; - private ApplicationUser _user = default!; - private IList? _currentLogins; - private IList? _otherLogins; - private bool _showRemoveButton; + private ApplicationUser user = default!; + private IList? currentLogins; + private IList? otherLogins; + private bool showRemoveButton; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] private string? LoginProvider { get; set; } @@ -88,65 +84,58 @@ [SupplyParameterFromQuery] private string? Action { get; set; } - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - protected override async Task OnInitializedAsync() { - _user = await UserAccessor.GetRequiredUserAsync(); - _currentLogins = await UserManager.GetLoginsAsync(_user); - _otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()) - .Where(auth => _currentLogins.All(ul => auth.Name != ul.LoginProvider)) + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + currentLogins = await UserManager.GetLoginsAsync(user); + otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider)) .ToList(); string? passwordHash = null; if (UserStore is IUserPasswordStore userPasswordStore) { - passwordHash = await userPasswordStore.GetPasswordHashAsync(_user, HttpContext.RequestAborted); + passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted); } - _showRemoveButton = passwordHash is not null || _currentLogins.Count > 1; + showRemoveButton = passwordHash is not null || currentLogins.Count > 1; if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction) { await OnGetLinkLoginCallbackAsync(); - return; } } private async Task OnSubmitAsync() { - var result = await UserManager.RemoveLoginAsync(_user, LoginProvider!, ProviderKey!); + var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!); if (!result.Succeeded) { - RedirectManager.RedirectToCurrentPageWithStatus("The external login was not removed."); - return; + RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext); } - await SignInManager.RefreshSignInAsync(_user); - RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed."); + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext); } private async Task OnGetLinkLoginCallbackAsync() { - var userId = await UserManager.GetUserIdAsync(_user); + var userId = await UserManager.GetUserIdAsync(user); var info = await SignInManager.GetExternalLoginInfoAsync(userId); - if (info == null) + if (info is null) { - RedirectManager.RedirectToCurrentPageWithStatus("Unexpected error occurred loading external login info."); - return; + RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext); } - var result = await UserManager.AddLoginAsync(_user, info); + var result = await UserManager.AddLoginAsync(user, info); if (!result.Succeeded) { - RedirectManager.RedirectToCurrentPageWithStatus("The external login was not added. External logins can only be associated with one account."); - return; + RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext); } // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - RedirectManager.RedirectToCurrentPageWithStatus("The external login was added."); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext); } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/GenerateRecoveryCodes.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor similarity index 62% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/GenerateRecoveryCodes.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor index 3de6f69e9c40..c63c765ab887 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/GenerateRecoveryCodes.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor @@ -2,18 +2,17 @@ @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor @inject IdentityRedirectManager RedirectManager @inject ILogger Logger Generate two-factor authentication (2FA) recovery codes -@if (_recoveryCodes is not null) +@if (recoveryCodes is not null) { - + } else { @@ -28,7 +27,7 @@ else

    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. + used in an authenticator app you should reset your authenticator keys.

    @@ -40,27 +39,29 @@ else } @code { - private ApplicationUser _user = default!; + private string? message; + private ApplicationUser user = default!; + private IEnumerable? recoveryCodes; - private IEnumerable? _recoveryCodes; - private string? _message; + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; protected override async Task OnInitializedAsync() { - _user = await UserAccessor.GetRequiredUserAsync(); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); - var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(_user); + var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user); if (!isTwoFactorEnabled) { - throw new InvalidOperationException($"Cannot generate recovery codes for user because they do not have 2FA enabled."); + 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."; + 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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Index.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Index.razor similarity index 50% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Index.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Index.razor index 7e59b2732987..2399b0166f38 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Index.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Index.razor @@ -1,15 +1,14 @@ @page "/Account/Manage" -@using System.ComponentModel.DataAnnotations; +@using System.ComponentModel.DataAnnotations @using System.Security.Claims -@using Microsoft.AspNetCore.Identity; -@using BlazorWeb_CSharp.Data; -@using BlazorWeb_CSharp.Identity +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data @inject AuthenticationStateProvider AuthenticationStateProvider @inject UserManager UserManager @inject SignInManager SignInManager -@inject UserAccessor UserAccessor; +@inject IdentityUserAccessor UserAccessor @inject IdentityRedirectManager RedirectManager Profile @@ -19,56 +18,56 @@
    - + - +
    - +
    - +
    - +
    @code { - private ApplicationUser _user = default!; - private string? _username; - private string? _phoneNumber; + private ApplicationUser user = default!; + private string? username; + private string? phoneNumber; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); protected override async Task OnInitializedAsync() { - Input ??= new(); - - _user = await UserAccessor.GetRequiredUserAsync(); - _username = await UserManager.GetUserNameAsync(_user); - _phoneNumber = await UserManager.GetPhoneNumberAsync(_user); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + username = await UserManager.GetUserNameAsync(user); + phoneNumber = await UserManager.GetPhoneNumberAsync(user); - Input.PhoneNumber ??= _phoneNumber; + Input.PhoneNumber ??= phoneNumber; } private async Task OnValidSubmitAsync() { - if (Input.PhoneNumber != _phoneNumber) + if (Input.PhoneNumber != phoneNumber) { - var setPhoneResult = await UserManager.SetPhoneNumberAsync(_user, Input.PhoneNumber); + var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber); if (!setPhoneResult.Succeeded) { - RedirectManager.RedirectToCurrentPageWithStatus("Unexpected error when trying to set phone number."); - return; + RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext); } } - await SignInManager.RefreshSignInAsync(_user); - RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated"); + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext); } private sealed class InputModel diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/PersonalData.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/PersonalData.razor similarity index 64% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/PersonalData.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/PersonalData.razor index b9b21c6990f0..851eb54c8820 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/PersonalData.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/PersonalData.razor @@ -1,10 +1,6 @@ @page "/Account/Manage/PersonalData" -@using System.Text.Json -@using Microsoft.AspNetCore.Identity -@using BlazorWeb_CSharp.Data - -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor Personal Data @@ -17,19 +13,22 @@

    Deleting this data will permanently remove your account, and this cannot be recovered.

    - +

    - Delete + Delete

    @code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + protected override async Task OnInitializedAsync() { - _ = await UserAccessor.GetRequiredUserAsync(); + _ = await UserAccessor.GetRequiredUserAsync(HttpContext); } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ResetAuthenticator.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ResetAuthenticator.razor similarity index 56% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ResetAuthenticator.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ResetAuthenticator.razor index dd951c7d2410..c12e38094f76 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ResetAuthenticator.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ResetAuthenticator.razor @@ -2,11 +2,10 @@ @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager -@inject UserAccessor UserAccessor +@inject IdentityUserAccessor UserAccessor @inject IdentityRedirectManager RedirectManager @inject ILogger Logger @@ -25,31 +24,29 @@

  • -
    + - +
    @code { - private ApplicationUser _user = default!; - - protected override async Task OnInitializedAsync() - { - _user = await UserAccessor.GetRequiredUserAsync(); - } + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; private async Task OnSubmitAsync() { - 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.", _user.Id); + 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); + 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."); + "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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/SetPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/SetPassword.razor similarity index 63% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/SetPassword.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/SetPassword.razor index 5f1aff4403ad..1fee353af5e4 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/SetPassword.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/SetPassword.razor @@ -3,33 +3,32 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager -@inject UserAccessor UserAccessor +@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.

    - + - +
    - +
    - +
    @@ -39,37 +38,37 @@
    @code { - private string? _message; - private ApplicationUser _user = default!; + private string? message; + private ApplicationUser user = default!; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; + private InputModel Input { get; set; } = new(); protected override async Task OnInitializedAsync() { - Input ??= new(); - - _user = await UserAccessor.GetRequiredUserAsync(); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); - var hasPassword = await UserManager.HasPasswordAsync(_user); + var hasPassword = await UserManager.HasPasswordAsync(user); if (hasPassword) { - RedirectManager.RedirectTo("/Account/Manage/ChangePassword"); - return; + RedirectManager.RedirectTo("Account/Manage/ChangePassword"); } } private async Task OnValidSubmitAsync() { - var addPasswordResult = await UserManager.AddPasswordAsync(_user, Input.NewPassword!); + var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!); if (!addPasswordResult.Succeeded) { - _message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}"; + message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}"; return; } - await SignInManager.RefreshSignInAsync(_user); - RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set."); + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext); } private sealed class InputModel diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/TwoFactorAuthentication.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/TwoFactorAuthentication.razor new file mode 100644 index 000000000000..d15097a9ed16 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/TwoFactorAuthentication.razor @@ -0,0 +1,101 @@ +@page "/Account/Manage/TwoFactorAuthentication" + +@using Microsoft.AspNetCore.Http.Features +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/_Imports.razor new file mode 100644 index 000000000000..ada5bb010a89 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/_Imports.razor @@ -0,0 +1,2 @@ +@layout ManageLayout +@attribute [Microsoft.AspNetCore.Authorization.Authorize] diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Register.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Register.razor new file mode 100644 index 000000000000..ac5c6045b066 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/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.Authentication +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using BlazorWeb_CSharp.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.

    +
    + +
    + + + +
    +
    + + + +
    +
    + + + +
    + +
    +
    +
    +
    +

    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 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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/RegisterConfirmation.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/RegisterConfirmation.razor new file mode 100644 index 000000000000..4473b82bba93 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/RegisterConfirmation.razor @@ -0,0 +1,68 @@ +@page "/Account/RegisterConfirmation" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using BlazorWeb_CSharp.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 +

    +} +else +{ +

    Please check your email to confirm your account.

    +} + +@code { + private string? emailConfirmationLink; + private string? statusMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery] + public string? Email { get; set; } + + [SupplyParameterFromQuery] + public string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + if (Email is null) + { + RedirectManager.RedirectTo(""); + } + + var user = await UserManager.FindByEmailAsync(Email); + if (user is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; + statusMessage = "Error finding user for unspecified email"; + } + else if (EmailSender is IdentityNoOpEmailSender) + { + // Once you add a real email sender, you should remove this code that lets you confirm the account + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + emailConfirmationLink = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResendEmailConfirmation.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResendEmailConfirmation.razor similarity index 65% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResendEmailConfirmation.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResendEmailConfirmation.razor index fa5f9d1dbafe..4cfebd2ddbad 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResendEmailConfirmation.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResendEmailConfirmation.razor @@ -7,10 +7,9 @@ @using Microsoft.AspNetCore.Identity.UI.Services @using Microsoft.AspNetCore.WebUtilities @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject UserManager UserManager -@inject IEmailSender EmailSender +@inject IEmailSender EmailSender @inject NavigationManager NavigationManager @inject IdentityRedirectManager RedirectManager @@ -19,14 +18,14 @@

    Resend email confirmation

    Enter your email.


    - +
    - +
    - +
    @@ -36,22 +35,17 @@
    @code { - private string? _message; + private string? message; [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; - - protected override void OnInitialized() - { - Input ??= new(); - } + private InputModel Input { get; set; } = new(); private async Task OnValidSubmitAsync() { var user = await UserManager.FindByEmailAsync(Input.Email!); if (user is null) { - _message = "Verification email sent. Please check your email."; + message = "Verification email sent. Please check your email."; return; } @@ -59,20 +53,17 @@ var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = NavigationManager.GetUriWithQueryParameters( - $"{NavigationManager.BaseUri}Account/ConfirmEmail", + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, new Dictionary { ["userId"] = userId, ["code"] = code }); - await EmailSender.SendEmailAsync( - Input.Email!, - "Confirm your email", - $"Please confirm your account by clicking here."); + await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); - _message = "Verification email sent. Please check your email."; + message = "Verification email sent. Please check your email."; } private sealed class InputModel { [Required] [EmailAddress] - public string? Email { get; set; } + public string Email { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResetPassword.razor similarity index 61% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPassword.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResetPassword.razor index 214c42643eb4..8c2d4c1f1239 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPassword.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResetPassword.razor @@ -6,7 +6,6 @@ @using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.WebUtilities @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject IdentityRedirectManager RedirectManager @inject UserManager UserManager @@ -19,23 +18,23 @@
    - + - +
    - +
    - +
    - +
    @@ -45,25 +44,24 @@
    @code { + private IEnumerable? identityErrors; + [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); [SupplyParameterFromQuery] private string? Code { get; set; } - IEnumerable? identityErrors; - private string? Message => identityErrors is null ? null : "Error: " + string.Join(", ", identityErrors.Select(error => error.Description)); + private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; protected override void OnInitialized() { if (Code is null) { - RedirectManager.RedirectTo("/Account/InvalidPasswordReset"); - } - else - { - Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + RedirectManager.RedirectTo("Account/InvalidPasswordReset"); } + + Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); } private async Task OnValidSubmitAsync() @@ -72,15 +70,13 @@ if (user is null) { // Don't reveal that the user does not exist - RedirectManager.RedirectTo("/Account/ResetPasswordConfirmation"); - return; + RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); } var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); if (result.Succeeded) { - RedirectManager.RedirectTo("/Account/ResetPasswordConfirmation"); - return; + RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); } identityErrors = result.Errors; @@ -90,19 +86,19 @@ { [Required] [EmailAddress] - public string Email { get; set; } = default!; + 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)] - public string Password { get; set; } = default!; + 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; } = default!; + public string ConfirmPassword { get; set; } = ""; [Required] - public string Code { get; set; } = default!; + public string Code { get; set; } = ""; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPasswordConfirmation.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResetPasswordConfirmation.razor similarity index 61% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPasswordConfirmation.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResetPasswordConfirmation.razor index 273c8247bd14..7f7347dccd5f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPasswordConfirmation.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResetPasswordConfirmation.razor @@ -3,5 +3,5 @@

    Reset password confirmation

    - Your password has been reset. Please click here to log in. + Your password has been reset. Please click here to log in.

    diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/_Imports.razor new file mode 100644 index 000000000000..ea096b9c8ff2 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/_Imports.razor @@ -0,0 +1,2 @@ +@using BlazorWeb_CSharp.Components.Account.Shared +@layout AccountLayout diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/PersistingRevalidatingAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs similarity index 55% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/PersistingRevalidatingAuthenticationStateProvider.cs rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs index 4bcaeb392093..32439c3c5322 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/PersistingRevalidatingAuthenticationStateProvider.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs @@ -9,31 +9,34 @@ using BlazorWeb_CSharp.Client; using BlazorWeb_CSharp.Data; -namespace BlazorWeb_CSharp.Identity; +namespace BlazorWeb_CSharp.Components.Account; -public class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider +// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user +// every 30 minutes an interactive circuit is connected. It also uses PersistentComponentState to flow the +// authentication state to the client which is then fixed for the lifetime of the WebAssembly application. +internal sealed class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider { - private readonly IServiceScopeFactory _scopeFactory; - private readonly PersistentComponentState _state; - private readonly IdentityOptions _options; + private readonly IServiceScopeFactory scopeFactory; + private readonly PersistentComponentState state; + private readonly IdentityOptions options; - private readonly PersistingComponentStateSubscription _subscription; + private readonly PersistingComponentStateSubscription subscription; - private Task? _authenticationStateTask; + private Task? authenticationStateTask; public PersistingRevalidatingAuthenticationStateProvider( ILoggerFactory loggerFactory, - IServiceScopeFactory scopeFactory, - PersistentComponentState state, - IOptions options) + IServiceScopeFactory serviceScopeFactory, + PersistentComponentState persistentComponentState, + IOptions optionsAccessor) : base(loggerFactory) { - _scopeFactory = scopeFactory; - _state = state; - _options = options.Value; + scopeFactory = serviceScopeFactory; + state = persistentComponentState; + options = optionsAccessor.Value; AuthenticationStateChanged += OnAuthenticationStateChanged; - _subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); + subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); } protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); @@ -42,7 +45,7 @@ protected override async Task ValidateAuthenticationStateAsync( AuthenticationState authenticationState, CancellationToken cancellationToken) { // Get the user manager from a new scope to ensure it fetches fresh data - await using var scope = _scopeFactory.CreateAsyncScope(); + await using var scope = scopeFactory.CreateAsyncScope(); var userManager = scope.ServiceProvider.GetRequiredService>(); return await ValidateSecurityStampAsync(userManager, authenticationState.User); } @@ -50,7 +53,7 @@ protected override async Task ValidateAuthenticationStateAsync( private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) { var user = await userManager.GetUserAsync(principal); - if (user == null) + if (user is null) { return false; } @@ -60,35 +63,35 @@ private async Task ValidateSecurityStampAsync(UserManager } else { - var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType); + var principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType); var userStamp = await userManager.GetSecurityStampAsync(user); return principalStamp == userStamp; } } - private void OnAuthenticationStateChanged(Task authenticationStateTask) + private void OnAuthenticationStateChanged(Task task) { - _authenticationStateTask = authenticationStateTask; + authenticationStateTask = task; } private async Task OnPersistingAsync() { - if (_authenticationStateTask is null) + if (authenticationStateTask is null) { - throw new UnreachableException($"Authentication state not set in {nameof(RevalidatingServerAuthenticationStateProvider)}.{nameof(OnPersistingAsync)}()."); + throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}()."); } - var authenticationState = await _authenticationStateTask; + var authenticationState = await authenticationStateTask; var principal = authenticationState.User; if (principal.Identity?.IsAuthenticated == true) { - var userId = principal.FindFirst(_options.ClaimsIdentity.UserIdClaimType)?.Value; - var email = principal.FindFirst(_options.ClaimsIdentity.EmailClaimType)?.Value; + var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value; + var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value; if (userId != null && email != null) { - _state.PersistAsJson(nameof(UserInfo), new UserInfo + state.PersistAsJson(nameof(UserInfo), new UserInfo { UserId = userId, Email = email, @@ -99,7 +102,7 @@ private async Task OnPersistingAsync() protected override void Dispose(bool disposing) { - _subscription.Dispose(); + subscription.Dispose(); AuthenticationStateChanged -= OnAuthenticationStateChanged; base.Dispose(disposing); } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingServerAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingServerAuthenticationStateProvider.cs new file mode 100644 index 000000000000..c7e967e77ace --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingServerAuthenticationStateProvider.cs @@ -0,0 +1,70 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using BlazorWeb_CSharp.Client; + +namespace BlazorWeb_CSharp.Components.Account; + +// This is a server-side AuthenticationStateProvider that uses PersistentComponentState to flow the +// authentication state to the client which is then fixed for the lifetime of the WebAssembly application. +internal sealed class PersistingServerAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable +{ + private readonly PersistentComponentState state; + private readonly IdentityOptions options; + + private readonly PersistingComponentStateSubscription subscription; + + private Task? authenticationStateTask; + + public PersistingServerAuthenticationStateProvider( + PersistentComponentState persistentComponentState, + IOptions optionsAccessor) + { + state = persistentComponentState; + options = optionsAccessor.Value; + + AuthenticationStateChanged += OnAuthenticationStateChanged; + subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); + } + + private void OnAuthenticationStateChanged(Task task) + { + authenticationStateTask = task; + } + + private async Task OnPersistingAsync() + { + if (authenticationStateTask is null) + { + throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}()."); + } + + var authenticationState = await authenticationStateTask; + var principal = authenticationState.User; + + if (principal.Identity?.IsAuthenticated == true) + { + var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value; + var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value; + + if (userId != null && email != null) + { + state.PersistAsJson(nameof(UserInfo), new UserInfo + { + UserId = userId, + Email = email, + }); + } + } + } + + public void Dispose() + { + subscription.Dispose(); + AuthenticationStateChanged -= OnAuthenticationStateChanged; + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/AccountLayout.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/AccountLayout.razor new file mode 100644 index 000000000000..4c0511d5d00a --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/AccountLayout.razor @@ -0,0 +1,32 @@ +@inherits LayoutComponentBase +@*#if (UseWebAssembly && InteractiveAtRoot) +@layout BlazorWeb_CSharp.Client.Layout.MainLayout +##else +@layout BlazorWeb_CSharp.Components.Layout.MainLayout +##endif*@ +@inject NavigationManager NavigationManager + +@if (HttpContext is null) +{ +

    Loading...

    +} +else +{ + @Body +} + +@code { + [CascadingParameter] + public HttpContext? HttpContext { get; set; } + + protected override void OnParametersSet() + { + if (HttpContext is null) + { + // If this code runs, we're currently rendering in interactive mode, so there is no HttpContext. + // The identity pages need to set cookies, so they require an HttpContext. To achieve this we + // must transition back from interactive mode to a server-rendered page. + NavigationManager.Refresh(forceReload: true); + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ExternalLoginPicker.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ExternalLoginPicker.razor similarity index 66% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ExternalLoginPicker.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ExternalLoginPicker.razor index 5c33681f7021..3127c8974a46 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ExternalLoginPicker.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ExternalLoginPicker.razor @@ -1,13 +1,11 @@ @using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Identity -@using BlazorWeb_CSharp.Components.Pages.Account @using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity @inject SignInManager SignInManager @inject IdentityRedirectManager RedirectManager -@if ((_externalLogins?.Count ?? 0) == 0) +@if (externalLogins.Length == 0) {

    @@ -18,12 +16,12 @@ } else { -

    +

    - @foreach (var provider in _externalLogins!) + @foreach (var provider in externalLogins) { } @@ -33,15 +31,13 @@ else } @code { - private IList? _externalLogins; + private AuthenticationScheme[] externalLogins = []; [SupplyParameterFromQuery] - private string ReturnUrl { get; set; } = default!; + private string? ReturnUrl { get; set; } protected override async Task OnInitializedAsync() { - ReturnUrl ??= "/"; - - _externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray(); } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageLayout.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ManageLayout.razor similarity index 93% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageLayout.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ManageLayout.razor index e4a7871bbc75..949bc92215cb 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageLayout.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ManageLayout.razor @@ -1,5 +1,5 @@ @inherits LayoutComponentBase -@layout MainLayout +@layout AccountLayout

    Manage your account

    diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ManageNavMenu.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ManageNavMenu.razor new file mode 100644 index 000000000000..29d1ec6e7d8a --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ManageNavMenu.razor @@ -0,0 +1,37 @@ +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data + +@inject SignInManager SignInManager + + + +@code { + private bool hasExternalLogins; + + protected override async Task OnInitializedAsync() + { + hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/RedirectToLogin.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/RedirectToLogin.razor new file mode 100644 index 000000000000..c8b8eff4af5c --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/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/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ShowRecoveryCodes.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ShowRecoveryCodes.razor similarity index 54% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ShowRecoveryCodes.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ShowRecoveryCodes.razor index cebec61aef19..aa92e119414c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ShowRecoveryCodes.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/ShowRecoveryCodes.razor @@ -10,23 +10,19 @@
    - @for (var row = 0; row < RecoveryCodes.Length; row += 2) + @foreach (var recoveryCode in RecoveryCodes) { - @RecoveryCodes[row] - -   - - @RecoveryCodes[row + 1] - -
    +
    + @recoveryCode +
    }
    @code { [Parameter] - public string[] RecoveryCodes { get; set; } = default!; + public string[] RecoveryCodes { get; set; } = []; [Parameter] - public string StatusMessage { get; set; } = default!; + public string? StatusMessage { get; set; } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/StatusMessage.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/StatusMessage.razor new file mode 100644 index 000000000000..12cd544cfe77 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/StatusMessage.razor @@ -0,0 +1,29 @@ +@if (!string.IsNullOrEmpty(DisplayMessage)) +{ + var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success"; + +} + +@code { + private string? messageFromCookie; + + [Parameter] + public string? Message { get; set; } + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + private string? DisplayMessage => Message ?? messageFromCookie; + + protected override void OnInitialized() + { + messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; + + if (messageFromCookie is not null) + { + HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor index a4d901abd856..aedb74932bc9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor @@ -15,6 +15,8 @@ ##endif*@ @*#if (!InteractiveAtRoot) + ##elseif (IndividualLocalAuth) + ##elseif (UseServer && UseWebAssembly) ##elseif (UseServer) @@ -27,7 +29,9 @@ @*#if (!InteractiveAtRoot) - ##elseif (UseServer && UseWebAssembly) --> + ##elseif (IndividualLocalAuth) + + ##elseif (UseServer && UseWebAssembly) ##elseif (UseServer) @@ -38,3 +42,35 @@ +@*#if (!InteractiveAtRoot || !IndividualLocalAuth) +#elseif (UseServer && UseWebAssembly) + +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account") + ? null + : InteractiveAuto; +} +#elseif (UseServer) + +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account") + ? null + : InteractiveServer; +} +#else + +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account") + ? null + : InteractiveWebAssembly; +} +#endif*@ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/LogoutForm.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/LogoutForm.razor deleted file mode 100644 index a08c78fc4cdc..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/LogoutForm.razor +++ /dev/null @@ -1,34 +0,0 @@ -@using Microsoft.AspNetCore.Identity -@using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity - -@inject SignInManager SignInManager -@inject NavigationManager NavigationManager -@inject IdentityRedirectManager RedirectManager - - - - - - -@code { - [Parameter(CaptureUnmatchedValues = true)] - public IDictionary? AdditionalAttributes { get; set; } - - [SupplyParameterFromForm] - private string? ReturnUrl { get; set; } - - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - private async Task OnSubmitAsync() - { - var user = HttpContext.User; - - if (SignInManager.IsSignedIn(user)) - { - await SignInManager.SignOutAsync(); - RedirectManager.RedirectTo(ReturnUrl ?? "/"); - } - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/StatusMessage.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/StatusMessage.razor deleted file mode 100644 index 43bcdc0478b6..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/StatusMessage.razor +++ /dev/null @@ -1,29 +0,0 @@ -@using BlazorWeb_CSharp.Identity - -@{ - var message = Message ?? MessageFromCookie; - - if (MessageFromCookie is not null) - { - HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); - } -} - -@if (!string.IsNullOrEmpty(message)) -{ - var statusMessageClass = message.StartsWith("Error") ? "danger" : "success"; - -} - -@code { - [Parameter] - public string? Message { get; set; } - - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - private string? MessageFromCookie => HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageNavMenu.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageNavMenu.razor deleted file mode 100644 index 8ffd7cd0a41e..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageNavMenu.razor +++ /dev/null @@ -1,37 +0,0 @@ -@using Microsoft.AspNetCore.Identity; -@using BlazorWeb_CSharp.Data; - -@inject SignInManager SignInManager; - - - -@code { - private bool _hasExternalLogins; - - protected override async Task OnInitializedAsync() - { - _hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor index ee04e1b8a0bf..aabb4413eb11 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor @@ -1,5 +1,5 @@ @*#if (IndividualLocalAuth) -@using BlazorWeb_CSharp.Components.Identity +@inject NavigationManager NavigationManager ##endif*@ +@*#if (IndividualLocalAuth) + +@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; + } +} +##endif*@ + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor.css b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor.css index 5dbb44248d7f..18e1a075347b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor.css @@ -81,13 +81,16 @@ padding-bottom: 1rem; } - .nav-item ::deep a { + .nav-item ::deep .nav-link { color: #d7d7d7; + background: none; + border: none; border-radius: 4px; height: 3rem; display: flex; align-items: center; line-height: 3rem; + width: 100%; } .nav-item ::deep a.active { @@ -95,7 +98,7 @@ color: white; } -.nav-item ::deep a:hover { +.nav-item ::deep .nav-link:hover { background-color: rgba(255,255,255,0.1); color: white; } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmail.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmail.razor deleted file mode 100644 index 992c6c59694f..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmail.razor +++ /dev/null @@ -1,49 +0,0 @@ -@page "/Account/ConfirmEmail" - -@using System.Text -@using Microsoft.AspNetCore.Identity -@using Microsoft.AspNetCore.WebUtilities -@using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity - -@inject UserManager UserManager -@inject IdentityRedirectManager RedirectManager - -Confirm email - -

    Confirm email

    - - -@code { - string? statusMessage; - - [SupplyParameterFromQuery] - public string? UserId { get; set; } - - [SupplyParameterFromQuery] - public string? Code { get; set; } - - protected override async Task OnInitializedAsync() - { - if (UserId == null || Code == null) - { - RedirectManager.RedirectTo("/"); - } - else - { - var user = await UserManager.FindByIdAsync(UserId); - if (user == null) - { - // Need a way to trigger a 404 from Blazor: https://github.com/dotnet/aspnetcore/issues/45654 - 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."; - } - } - } -} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Email.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Email.razor deleted file mode 100644 index c629d0a8ee8e..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Email.razor +++ /dev/null @@ -1,123 +0,0 @@ -@page "/Account/Manage/Email" - -@using System.ComponentModel.DataAnnotations -@using System.Text -@using System.Text.Encodings.Web -@using Microsoft.AspNetCore.Identity -@using Microsoft.AspNetCore.Identity.UI.Services -@using Microsoft.AspNetCore.WebUtilities -@using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity - -@inject UserManager UserManager -@inject UserAccessor UserAccessor -@inject IEmailSender EmailSender -@inject NavigationManager NavigationManager -@inject IdentityRedirectManager RedirectManager - -Manage email - -

    Manage email

    - - -
    -
    -
    - - - - - - @if (_isEmailConfirmed) - { -
    - -
    - -
    - -
    - } - else - { -
    - - - -
    - } -
    - - - -
    - -
    -
    -
    - -@code { - private ApplicationUser _user = default!; - private string? _email; - private bool _isEmailConfirmed; - - [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; - - protected override async Task OnInitializedAsync() - { - Input ??= new(); - - _user = await UserAccessor.GetRequiredUserAsync(); - _email = await UserManager.GetEmailAsync(_user); - _isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(_user); - - Input.NewEmail ??= _email; - } - - private async Task OnValidSubmitAsync() - { - if (Input.NewEmail != _email) - { - var userId = await UserManager.GetUserIdAsync(_user); - var code = await UserManager.GenerateChangeEmailTokenAsync(_user, Input.NewEmail!); - code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); - var callbackUrl = NavigationManager.GetUriWithQueryParameters( - $"{NavigationManager.BaseUri}Account/ConfirmEmailChange", - new Dictionary { { "userId", userId }, { "email", Input.NewEmail }, { "code", code } }); - await EmailSender.SendEmailAsync( - Input.NewEmail!, - "Confirm your email", - $"Please confirm your account by clicking here."); - - RedirectManager.RedirectToCurrentPageWithStatus("Confirmation link to change email sent. Please check your email."); - return; - } - - RedirectManager.RedirectToCurrentPageWithStatus("Your email is unchanged."); - } - - private async Task OnSendEmailVerificationAsync() - { - var userId = await UserManager.GetUserIdAsync(_user); - var code = await UserManager.GenerateEmailConfirmationTokenAsync(_user); - code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); - var callbackUrl = NavigationManager.GetUriWithQueryParameters( - $"{NavigationManager.BaseUri}Account/ConfirmEmail", - new Dictionary { { "userId", userId }, { "code", code } }); - await EmailSender.SendEmailAsync( - _email!, - "Confirm your email", - $"Please confirm your account by clicking here."); - - RedirectManager.RedirectToCurrentPageWithStatus("Verification email sent. Please check your email."); - } - - private sealed class InputModel - { - [Required] - [EmailAddress] - [Display(Name = "New email")] - public string? NewEmail { get; set; } - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/TwoFactorAuthentication.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/TwoFactorAuthentication.razor deleted file mode 100644 index fbb2f32eddbd..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/TwoFactorAuthentication.razor +++ /dev/null @@ -1,101 +0,0 @@ -@page "/Account/Manage/TwoFactorAuthentication" - -@using Microsoft.AspNetCore.Http.Features -@using Microsoft.AspNetCore.Identity -@using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity - -@inject UserManager UserManager -@inject SignInManager SignInManager -@inject UserAccessor UserAccessor -@inject IdentityRedirectManager RedirectManager - -Two-factor authentication (2FA) - - -

    Two-factor authentication (2FA)

    -@if (_consentFeature?.CanTrack ?? true) -{ - 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 ApplicationUser? _user; - private ITrackingConsentFeature? _consentFeature; - private bool _hasAuthenticator; - private int _recoveryCodesLeft; - private bool _is2faEnabled; - private bool _isMachineRemembered; - - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - protected override async Task OnInitializedAsync() - { - _user = await UserAccessor.GetRequiredUserAsync(); - _consentFeature = HttpContext.Features.Get(); - _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."); - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/_Imports.razor deleted file mode 100644 index d605aec1f504..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/_Imports.razor +++ /dev/null @@ -1,2 +0,0 @@ -@layout BlazorWeb_CSharp.Components.Layout.ManageLayout -@attribute [Microsoft.AspNetCore.Authorization.Authorize] diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Register.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Register.razor deleted file mode 100644 index da7058817e02..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Register.razor +++ /dev/null @@ -1,176 +0,0 @@ -@page "/Account/Register" - -@using System.ComponentModel.DataAnnotations -@using System.Text -@using System.Text.Encodings.Web -@using Microsoft.AspNetCore.Authentication -@using Microsoft.AspNetCore.Identity -@using Microsoft.AspNetCore.Identity.UI.Services -@using Microsoft.AspNetCore.WebUtilities -@using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity - -@inject UserManager UserManager -@inject IUserStore UserStore -@inject SignInManager SignInManager -@inject ILogger Logger -@inject IEmailSender EmailSender -@inject NavigationManager NavigationManager -@inject IdentityRedirectManager RedirectManager - -Register - -

    Register

    - -
    -
    - - - -

    Create a new account.

    -
    - -
    - - - -
    -
    - - - -
    -
    - - - -
    - -
    -
    -
    -
    -

    Use another service to register.

    -
    - -
    -
    -
    - -@code { - [SupplyParameterFromForm] - public InputModel Input { get; set; } = default!; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [SupplyParameterFromQuery] - public string ReturnUrl { get; set; } = ""; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public class InputModel - { - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [Required] - [EmailAddress] - [Display(Name = "Email")] - public string Email { get; set; } = null!; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [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; } = null!; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [DataType(DataType.Password)] - [Display(Name = "Confirm password")] - [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] - public string ConfirmPassword { get; set; } = null!; - } - - IEnumerable? identityErrors; - string? Message => identityErrors is null ? null : "Error: " + string.Join(", ", identityErrors.Select(error => error.Description)); - - protected override void OnInitialized() - { - Input ??= new(); - } - - 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) - { - 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.SendEmailAsync(Input.Email, "Confirm your email", - $"Please confirm your account by clicking here."); - - if (UserManager.Options.SignIn.RequireConfirmedAccount) - { - RedirectManager.RedirectTo( - "/Account/RegisterConfirmation", - new() { ["Email"] = Input.Email, ["ReturnUrl"] = ReturnUrl }); - } - else - { - await SignInManager.SignInAsync(user, isPersistent: false); - RedirectManager.RedirectTo(ReturnUrl); - } - } - else - { - identityErrors = result.Errors; - } - } - - private 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; - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/RegisterConfirmation.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/RegisterConfirmation.razor deleted file mode 100644 index 5f54beb59e9a..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/RegisterConfirmation.razor +++ /dev/null @@ -1,67 +0,0 @@ -@page "/Account/RegisterConfirmation" - -@using System.Text -@using Microsoft.AspNetCore.Identity -@using Microsoft.AspNetCore.Identity.UI.Services -@using Microsoft.AspNetCore.WebUtilities -@using BlazorWeb_CSharp.Data -@using BlazorWeb_CSharp.Identity - -@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 -

    -} -else -{ -

    Please check your email to confirm your account.

    -} - -@code { - string? emailConfirmationLink; - string? statusMessage; - - [SupplyParameterFromQuery] public string? Email { get; set; } - [SupplyParameterFromQuery] public string ReturnUrl { get; set; } = "/"; - - protected override async Task OnInitializedAsync() - { - if (Email == null) - { - RedirectManager.RedirectTo("/"); - } - else - { - - var user = await UserManager.FindByEmailAsync(Email); - if (user == null) - { - // Need a way to trigger a 404 from Blazor: https://github.com/dotnet/aspnetcore/issues/45654 - statusMessage = $"Error finding user for unspecified email"; - } - else if (EmailSender is NoOpEmailSender) - { - // Once you add a real email sender, you should remove this code that lets you confirm the account - var userId = await UserManager.GetUserIdAsync(user); - var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); - code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); - emailConfirmationLink = NavigationManager.GetUriWithQueryParameters( - NavigationManager.ToAbsoluteUri("/Account/ConfirmEmail").AbsoluteUri, - new Dictionary { { "userId", userId }, { "code", code }, { "returnUrl", ReturnUrl } }); - } - } - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/_Imports.razor deleted file mode 100644 index dfb627bc86ad..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@using BlazorWeb_CSharp.Components.Identity diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor index ee6f5865a0c1..927fa988e07b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor @@ -25,7 +25,8 @@

    @code{ - [CascadingParameter] public HttpContext? HttpContext { get; set; } + [CascadingParameter] + public HttpContext? HttpContext { get; set; } public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor index 1426521804d1..ee69ff447329 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor @@ -1,11 +1,18 @@ -@*#if (UseWebAssembly && !InteractiveAtRoot) +@*#if (IndividualLocalAuth && !UseWebAssembly) +@using BlazorWeb_CSharp.Components.Account.Shared +##endif*@ +@*#if (UseWebAssembly && !InteractiveAtRoot) ##else ##endif*@ @*#if (IndividualLocalAuth) - + + + + + ##else ##endif*@ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserAccessor.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserAccessor.cs deleted file mode 100644 index f6fefe902c63..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserAccessor.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using BlazorWeb_CSharp.Identity; - -namespace BlazorWeb_CSharp.Data; - -internal sealed class UserAccessor( - IHttpContextAccessor httpContextAccessor, - UserManager userManager, - IdentityRedirectManager redirectManager) -{ - public async Task GetRequiredUserAsync() - { - var principal = httpContextAccessor.HttpContext?.User ?? - throw new InvalidOperationException($"{nameof(GetRequiredUserAsync)} requires access to an {nameof(HttpContext)}."); - - var user = await userManager.GetUserAsync(principal); - - if (user is null) - { - // Throws NavigationException, which is handled by the framework as a redirect. - redirectManager.RedirectToWithStatus("/Account/InvalidUser", "Error: Unable to load user with ID '{userManager.GetUserId(principal)}'."); - } - - return user; - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/app.db b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/app.db similarity index 100% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/app.db rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/app.db diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/IdentityRedirectManager.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/IdentityRedirectManager.cs deleted file mode 100644 index 2a1cccde8fc4..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/IdentityRedirectManager.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Components; - -namespace BlazorWeb_CSharp.Identity; - -internal sealed class IdentityRedirectManager( - NavigationManager navigationManager, - IHttpContextAccessor httpContextAccessor) -{ - public const string StatusCookieName = "Identity.StatusMessage"; - - private static readonly CookieBuilder _statusCookieBuilder = new CookieBuilder - { - SameSite = SameSiteMode.Strict, - HttpOnly = true, - IsEssential = true, - MaxAge = TimeSpan.FromSeconds(5), - }; - - [DoesNotReturn] - public void RedirectTo(string uri) - { - if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) - { - uri = navigationManager.ToBaseRelativePath(uri); - } - - // This works because either: - // [1] NavigateTo() throws NavigationException, which is handled by the framework as a redirect. - // [2] NavigateTo() throws some other exception, which gets treated as a normal unhandled exception. - // [3] NavigateTo() does not throw an exception, meaning we're not rendering from an endpoint, so we throw - // an InvalidOperationException to indicate that we can't redirect. - navigationManager.NavigateTo(uri); - throw new InvalidOperationException($"Can only redirect when rendering from an endpoint."); - } - - [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) - { - var httpContext = httpContextAccessor.HttpContext ?? - throw new InvalidOperationException($"{nameof(RedirectToWithStatus)} requires access to an {nameof(HttpContext)}."); - httpContext.Response.Cookies.Append(StatusCookieName, message, _statusCookieBuilder.Build(httpContext)); - - RedirectTo(uri); - } - - [DoesNotReturn] - public void RedirectToCurrentPage() - => RedirectTo(navigationManager.Uri); - - [DoesNotReturn] - public void RedirectToCurrentPageWithStatus(string message) - => RedirectToWithStatus(navigationManager.Uri, message); -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/PersistingServerAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/PersistingServerAuthenticationStateProvider.cs deleted file mode 100644 index 6149f109e221..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/PersistingServerAuthenticationStateProvider.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Components.Server; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using BlazorWeb_CSharp.Client; - -namespace BlazorWeb_CSharp.Identity; - -public class PersistingServerAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable -{ - private readonly PersistentComponentState _state; - private readonly IdentityOptions _options; - - private readonly PersistingComponentStateSubscription _subscription; - - private Task? _authenticationStateTask; - - public PersistingServerAuthenticationStateProvider(PersistentComponentState state, IOptions options) - { - _state = state; - _options = options.Value; - - AuthenticationStateChanged += OnAuthenticationStateChanged; - _subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); - } - - private void OnAuthenticationStateChanged(Task authenticationStateTask) - { - _authenticationStateTask = authenticationStateTask; - } - - private async Task OnPersistingAsync() - { - if (_authenticationStateTask is null) - { - throw new UnreachableException($"Authentication state not set in {nameof(RevalidatingServerAuthenticationStateProvider)}.{nameof(OnPersistingAsync)}()."); - } - - var authenticationState = await _authenticationStateTask; - var principal = authenticationState.User; - - if (principal.Identity?.IsAuthenticated == true) - { - var userId = principal.FindFirst(_options.ClaimsIdentity.UserIdClaimType)?.Value; - var email = principal.FindFirst(_options.ClaimsIdentity.EmailClaimType)?.Value; - - if (userId != null && email != null) - { - _state.PersistAsJson(nameof(UserInfo), new UserInfo - { - UserId = userId, - Email = email, - }); - } - } - } - - public void Dispose() - { - _subscription.Dispose(); - AuthenticationStateChanged -= OnAuthenticationStateChanged; - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.Main.cs index 1777ce9dfdd7..bf9df79e72fc 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.Main.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.Main.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Components.Server; #endif using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.EntityFrameworkCore; #endif #if (UseWebAssembly && SampleContent) @@ -12,10 +11,8 @@ #endif using BlazorWeb_CSharp.Components; #if (IndividualLocalAuth) +using BlazorWeb_CSharp.Components.Account; using BlazorWeb_CSharp.Data; -#if (UseServer || UseWebAssembly) -using BlazorWeb_CSharp.Identity; -#endif #endif namespace BlazorWeb_CSharp; @@ -43,7 +40,7 @@ public static void Main(string[] args) #if (IndividualLocalAuth) builder.Services.AddCascadingAuthenticationState(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); #if (UseServer && UseWebAssembly) builder.Services.AddScoped(); @@ -58,7 +55,11 @@ public static void Main(string[] args) #if (!UseServer) builder.Services.AddAuthorization(); #endif - builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) + builder.Services.AddAuthentication(options => + { + options.DefaultScheme = IdentityConstants.ApplicationScheme; + options.DefaultSignInScheme = IdentityConstants.ExternalScheme; + }) .AddIdentityCookies(); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); @@ -75,16 +76,21 @@ public static void Main(string[] args) .AddSignInManager() .AddDefaultTokenProviders(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton, IdentityNoOpEmailSender>(); #endif var app = builder.Build(); // Configure the HTTP request pipeline. -#if (UseWebAssembly) +#if (UseWebAssembly || IndividualLocalAuth) if (app.Environment.IsDevelopment()) { +#if (UseWebAssembly) app.UseWebAssemblyDebugging(); +#endif +#if (IndividualLocalAuth) + app.UseMigrationsEndPoint(); +#endif } else #else diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs index a44f6beed12a..49118765d29c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Components.Server; #endif using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.EntityFrameworkCore; #endif #if (UseWebAssembly && SampleContent) @@ -12,10 +11,8 @@ #endif using BlazorWeb_CSharp.Components; #if (IndividualLocalAuth) +using BlazorWeb_CSharp.Components.Account; using BlazorWeb_CSharp.Data; -#if (UseServer || UseWebAssembly) -using BlazorWeb_CSharp.Identity; -#endif #endif var builder = WebApplication.CreateBuilder(args); @@ -37,7 +34,7 @@ #if (IndividualLocalAuth) builder.Services.AddCascadingAuthenticationState(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); #if (UseServer && UseWebAssembly) builder.Services.AddScoped(); @@ -52,7 +49,11 @@ #if (!UseServer) builder.Services.AddAuthorization(); #endif -builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) +builder.Services.AddAuthentication(options => + { + options.DefaultScheme = IdentityConstants.ApplicationScheme; + options.DefaultSignInScheme = IdentityConstants.ExternalScheme; + }) .AddIdentityCookies(); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); @@ -69,16 +70,21 @@ .AddSignInManager() .AddDefaultTokenProviders(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton, IdentityNoOpEmailSender>(); #endif var app = builder.Build(); // Configure the HTTP request pipeline. -#if (UseWebAssembly) +#if (UseWebAssembly || IndividualLocalAuth) if (app.Environment.IsDevelopment()) { +#if (UseWebAssembly) app.UseWebAssemblyDebugging(); +#endif +#if (IndividualLocalAuth) + app.UseMigrationsEndPoint(); +#endif } else #else diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Properties/launchSettings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Properties/launchSettings.json index c6a17ec14598..8963735edf8f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Properties/launchSettings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Properties/launchSettings.json @@ -21,7 +21,7 @@ //#if (UseWebAssembly) "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", //#endif - "applicationUrl": "http://localhost:5000", + "applicationUrl": "http://localhost:5500", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -35,7 +35,7 @@ //#if (UseWebAssembly) "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", //#endif - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "applicationUrl": "https://localhost:5501;http://localhost:5500", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/appsettings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/appsettings.json index a3f1a98503ab..3a18c82e2dc6 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/appsettings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/appsettings.json @@ -4,7 +4,7 @@ //#if (UseLocalDB) // "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-BlazorWeb_CSharp-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" //#else -// "DefaultConnection": "DataSource=app.db;Cache=Shared" +// "DefaultConnection": "DataSource=Data\\app.db;Cache=Shared" //#endif // }, ////#endif diff --git a/src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWasmTemplateTest.cs b/src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWasmTemplateTest.cs index a351461caa93..9a781f2b9cf1 100644 --- a/src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWasmTemplateTest.cs +++ b/src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWasmTemplateTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.CommandLineUtils; -using Newtonsoft.Json.Linq; using Microsoft.Playwright; using Templates.Test.Helpers; @@ -23,7 +22,8 @@ public BlazorWasmTemplateTest(ProjectFactoryFixture projectFactory) public override string ProjectType { get; } = "blazorwasm"; - [Theory(Skip="https://github.com/dotnet/aspnetcore/issues/47225")] + [Theory] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/47225")] [InlineData(BrowserKind.Chromium)] public async Task BlazorWasmStandaloneTemplate_Works(BrowserKind browserKind) { @@ -316,7 +316,7 @@ private static async Task TestBasicNavigation(string appName, IPage page, bool u // Initially displays the home page await page.WaitForSelectorAsync("h1 >> text=Hello, world!"); - Assert.Equal("Index", (await page.TitleAsync()).Trim()); + Assert.Equal("Home", (await page.TitleAsync()).Trim()); // Can navigate to the counter page await Task.WhenAll( @@ -372,11 +372,8 @@ await Task.WhenAll( if (!skipFetchData) { - // Can navigate to the 'fetch data' page - await Task.WhenAll( - page.WaitForNavigationAsync(new() { UrlString = "**/fetchdata" }), - page.WaitForSelectorAsync("h1 >> text=Weather forecast"), - page.ClickAsync("text=Fetch data")); + await page.ClickAsync("a[href=weather]"); + await page.WaitForSelectorAsync("h1 >> text=Weather"); // Asynchronously loads and displays the table of weather forecasts await page.WaitForSelectorAsync("table>tbody>tr"); diff --git a/src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWebTemplateTest.cs b/src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWebTemplateTest.cs deleted file mode 100644 index 31b79308c7f5..000000000000 --- a/src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWebTemplateTest.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Testing; -using Templates.Test.Helpers; -using Xunit.Abstractions; - -namespace Templates.Blazor.Tests; -public class BlazorWebTemplateTest : LoggedTest -{ - public BlazorWebTemplateTest(ProjectFactoryFixture projectFactory) - { - ProjectFactory = projectFactory; - } - - public ProjectFactoryFixture ProjectFactory { get; set; } - - private ITestOutputHelper _output; - public ITestOutputHelper Output - { - get - { - if (_output == null) - { - _output = new TestOutputLogger(Logger); - } - return _output; - } - } - - [ConditionalTheory] - [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] - [MemberData(nameof(ArgsData))] - public async Task BlazorWebTemplate_NoAuth(string[] args) - { - var project = await ProjectFactory.CreateProject(Output); - - await project.RunDotNetNewAsync("blazor", args: args); - - var expectedLaunchProfileNames = args.Contains(ArgConstants.NoHttps) - ? new[] { "http", "IIS Express" } - : new[] { "http", "https", "IIS Express" }; - await project.VerifyLaunchSettings(expectedLaunchProfileNames); - - var projectFileContents = ReadFile(project.TemplateOutputDir, $"{project.ProjectName}.csproj"); - Assert.DoesNotContain(".db", projectFileContents); - Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools", projectFileContents); - Assert.DoesNotContain("Microsoft.VisualStudio.Web.CodeGeneration.Design", projectFileContents); - Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools.DotNet", projectFileContents); - Assert.DoesNotContain("Microsoft.Extensions.SecretManager.Tools", projectFileContents); - - await project.RunDotNetPublishAsync(); - - // Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release - // The output from publish will go into bin/Release/netcoreappX.Y/publish and won't be affected by calling build - // later, while the opposite is not true. - - await project.RunDotNetBuildAsync(); - - var pages = new List - { - new Page - { - Url = BlazorTemplatePages.Index, - }, - new Page - { - Url = BlazorTemplatePages.Weather, - } - }; - - if (!args.Contains(ArgConstants.NoInteractivity)) - { - pages.Add(new Page - { - Url = BlazorTemplatePages.Counter, - }); - } - - using (var aspNetProcess = project.StartBuiltProjectAsync()) - { - Assert.False( - aspNetProcess.Process.HasExited, - ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process)); - - await aspNetProcess.AssertPagesOk(pages); - } - - using (var aspNetProcess = project.StartPublishedProjectAsync()) - { - Assert.False( - aspNetProcess.Process.HasExited, - ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", project, aspNetProcess.Process)); - - await aspNetProcess.AssertPagesOk(pages); - } - } - - public static TheoryData ArgsData() => new TheoryData - { - new string[0], - new[] { ArgConstants.UseProgramMain }, - new[] { ArgConstants.NoHttps }, - new[] { ArgConstants.UseProgramMain, ArgConstants.NoHttps }, - new[] { ArgConstants.NoInteractivity }, - new[] { ArgConstants.NoInteractivity, ArgConstants.UseProgramMain }, - new[] { ArgConstants.NoInteractivity, ArgConstants.NoHttps }, - new[] { ArgConstants.NoInteractivity, ArgConstants.UseProgramMain, ArgConstants.NoHttps } - }; - - private string ReadFile(string basePath, string path) - { - var fullPath = Path.Combine(basePath, path); - var doesExist = File.Exists(fullPath); - - Assert.True(doesExist, $"Expected file to exist, but it doesn't: {path}"); - return File.ReadAllText(Path.Combine(basePath, path)); - } - - private class BlazorTemplatePages - { - internal static readonly string Index = ""; - internal static readonly string Weather = "weather"; - internal static readonly string Counter = "counter"; - } -} diff --git a/src/ProjectTemplates/test/Templates.Blazor.Tests/PlaywrightFixture.cs b/src/ProjectTemplates/test/Templates.Blazor.Tests/PlaywrightFixture.cs deleted file mode 100644 index 2e1c84602b73..000000000000 --- a/src/ProjectTemplates/test/Templates.Blazor.Tests/PlaywrightFixture.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Microsoft.AspNetCore.BrowserTesting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging.Testing; -using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace ProjectTemplates.Tests.Infrastructure; - -public class PlaywrightFixture : IAsyncLifetime -{ - private static readonly bool _isCIEnvironment = - !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ContinuousIntegrationBuild")) || - !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("Helix")); - - private readonly IMessageSink _diagnosticsMessageSink; - private static readonly BrowserManagerConfiguration _config = new BrowserManagerConfiguration(CreateConfiguration(typeof(TTestAssemblyType).Assembly)); - - public PlaywrightFixture(IMessageSink diagnosticsMessageSink) - { - _diagnosticsMessageSink = diagnosticsMessageSink; - } - - private static IConfiguration CreateConfiguration(Assembly assembly) - { - var basePath = Path.GetDirectoryName(assembly.Location); - var os = Environment.OSVersion.Platform switch - { - PlatformID.Win32NT => "win", - PlatformID.Unix => "linux", - PlatformID.MacOSX => "osx", - _ => null - }; - - var builder = new ConfigurationBuilder() - .AddJsonFile(Path.Combine(basePath, "playwrightSettings.json")) - .AddJsonFile(Path.Combine(basePath, $"playwrightSettings.{os}.json"), optional: true); - - if (_isCIEnvironment) - { - builder.AddJsonFile(Path.Combine(basePath, "playwrightSettings.ci.json"), optional: true) - .AddJsonFile(Path.Combine(basePath, $"playwrightSettings.ci.{os}.json"), optional: true); - } - - if (Debugger.IsAttached) - { - builder.AddJsonFile(Path.Combine(basePath, "playwrightSettings.debug.json"), optional: true); - } - - return builder.Build(); - } - - public async Task InitializeAsync() - { - var sink = new TestSink(); - sink.MessageLogged += LogBrowserManagerMessage; - var factory = new TestLoggerFactory(sink, enabled: true); - BrowserManager = await BrowserManager.CreateAsync(_config, factory); - } - - private void LogBrowserManagerMessage(WriteContext context) - { - _diagnosticsMessageSink.OnMessage(new DiagnosticMessage(context.Message)); - } - - public async Task DisposeAsync() - { - await BrowserManager.DisposeAsync(); - } - - public BrowserManager BrowserManager { get; set; } -} diff --git a/src/ProjectTemplates/test/Templates.Mvc.Tests/BlazorTemplateTest.cs b/src/ProjectTemplates/test/Templates.Mvc.Tests/BlazorTemplateTest.cs new file mode 100644 index 000000000000..8ceb1be427de --- /dev/null +++ b/src/ProjectTemplates/test/Templates.Mvc.Tests/BlazorTemplateTest.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Testing; +using Templates.Test.Helpers; +using Xunit.Sdk; + +namespace Templates.Mvc.Test; + +public class BlazorTemplateTest : LoggedTest +{ + public BlazorTemplateTest(ProjectFactoryFixture projectFactory) + { + ProjectFactory = projectFactory; + } + + public ProjectFactoryFixture ProjectFactory { get; set; } + + public static TheoryData ArgsData() => + [ + [], + [ArgConstants.UseProgramMain], + [ArgConstants.NoHttps], + [ArgConstants.Empty], + [ArgConstants.NoInteractivity], + [ArgConstants.WebAssemblyInteractivity], + [ArgConstants.AutoInteractivity], + [ArgConstants.GlobalInteractivity], + [ArgConstants.GlobalInteractivity, ArgConstants.WebAssemblyInteractivity], + [ArgConstants.GlobalInteractivity, ArgConstants.AutoInteractivity], + [ArgConstants.NoInteractivity, ArgConstants.UseProgramMain, ArgConstants.NoHttps, ArgConstants.Empty], + ]; + + [ConditionalTheory] + [MemberData(nameof(ArgsData))] + [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] + public Task BlazorWebTemplate_NoAuth(string[] args) => BlazorWebTemplate_Core(args); + + [ConditionalTheory] + [MemberData(nameof(ArgsData))] + [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] + public Task BlazorWebTemplate_IndividualAuth(string[] args) => BlazorWebTemplate_Core([ArgConstants.IndividualAuth, ..args]); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + [SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "No LocalDb on non-Windows")] + public Task BlazorWebTemplate_IndividualAuth_LocalDb(bool useProgramMain) => useProgramMain + ? BlazorWebTemplate_Core([ArgConstants.IndividualAuth, ArgConstants.UseLocalDb, ArgConstants.UseProgramMain]) + : BlazorWebTemplate_Core([ArgConstants.IndividualAuth, ArgConstants.UseLocalDb]); + + private async Task BlazorWebTemplate_Core(string[] args) + { + var project = await ProjectFactory.CreateProject(TestOutputHelper); + + await project.RunDotNetNewAsync("blazor", args: args); + + var expectedLaunchProfileNames = args.Contains(ArgConstants.NoHttps) + ? new[] { "http", "IIS Express" } + : new[] { "http", "https", "IIS Express" }; + await project.VerifyLaunchSettings(expectedLaunchProfileNames); + + var projectFileContents = await ReadProjectFileAsync(project); + Assert.DoesNotContain("Microsoft.VisualStudio.Web.CodeGeneration.Design", projectFileContents); + Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools.DotNet", projectFileContents); + Assert.DoesNotContain("Microsoft.Extensions.SecretManager.Tools", projectFileContents); + + if (!args.Contains(ArgConstants.IndividualAuth)) + { + Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools", projectFileContents); + Assert.DoesNotContain(".db", projectFileContents); + } + else + { + Assert.Contains("Microsoft.EntityFrameworkCore.Tools", projectFileContents); + + if (args.Contains(ArgConstants.UseLocalDb)) + { + Assert.DoesNotContain(".db", projectFileContents); + } + else + { + Assert.Contains(".db", projectFileContents); + } + } + + // This can be removed once https://github.com/dotnet/razor/issues/9343 is fixed. + await WorkAroundNonNullableRenderModeAsync(project); + + // Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release + // The output from publish will go into bin/Release/netcoreappX.Y/publish and won't be affected by calling build + // later, while the opposite is not true. + await project.RunDotNetPublishAsync(); + await project.RunDotNetBuildAsync(); + var expectedPages = GetExpectedPages(args); + var unexpectedPages = GetUnxpectedPages(args); + + async Task VerifyProcessAsync(AspNetProcess process) + { + Assert.False( + process.Process.HasExited, + ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, process.Process)); + + await process.AssertPagesOk(expectedPages); + await process.AssertPagesNotFound(unexpectedPages); + + if (args.Contains(ArgConstants.IndividualAuth) && !args.Contains(ArgConstants.Empty)) + { + var response = await process.SendRequest(BlazorTemplatePages.Auth); + response.EnsureSuccessStatusCode(); + Assert.Equal("/Account/Login?ReturnUrl=%2Fauth", response.RequestMessage.RequestUri.PathAndQuery); + } + } + + using (var process = project.StartBuiltProjectAsync()) + { + await VerifyProcessAsync(process); + } + using (var process = project.StartPublishedProjectAsync()) + { + await VerifyProcessAsync(process); + } + } + + private static IEnumerable GetExpectedPages(string[] args) + { + yield return new(BlazorTemplatePages.Index); + + if (args.Contains(ArgConstants.IndividualAuth)) + { + yield return new(BlazorTemplatePages.Login); + yield return new(BlazorTemplatePages.Register); + yield return new(BlazorTemplatePages.ForgotPassword); + yield return new(BlazorTemplatePages.ResendEmailConfirmation); + } + + if (args.Contains(ArgConstants.Empty)) + { + yield break; + } + + yield return new(BlazorTemplatePages.Weather); + + if (args.Contains(ArgConstants.NoInteractivity)) + { + yield break; + } + + yield return new(BlazorTemplatePages.Counter); + } + + private static IEnumerable GetUnxpectedPages(string[] args) + { + if (!args.Contains(ArgConstants.IndividualAuth)) + { + yield return BlazorTemplatePages.Auth; + yield return BlazorTemplatePages.Login; + } + + if (args.Contains(ArgConstants.Empty)) + { + yield return BlazorTemplatePages.Weather; + yield return BlazorTemplatePages.Counter; + yield return BlazorTemplatePages.Auth; + } + + if (args.Contains(ArgConstants.NoInteractivity)) + { + yield return BlazorTemplatePages.Counter; + } + } + + private Task ReadProjectFileAsync(Project project) + { + var singleProjectPath = Path.Combine(project.TemplateOutputDir, $"{project.ProjectName}.csproj"); + if (File.Exists(singleProjectPath)) + { + return File.ReadAllTextAsync(singleProjectPath); + } + + var multiProjectPath = Path.Combine(project.TemplateOutputDir, project.ProjectName, $"{project.ProjectName}.csproj"); + if (File.Exists(multiProjectPath)) + { + // Change the TemplateOutputDir to that of the main project. + project.TemplateOutputDir = Path.GetDirectoryName(multiProjectPath); + return File.ReadAllTextAsync(multiProjectPath); + } + + throw new FailException($"Expected file to exist, but it doesn't: {singleProjectPath}"); + } + + private async Task WorkAroundNonNullableRenderModeAsync(Project project) + { + var appRazorPath = Path.Combine(project.TemplateOutputDir, "Components", "App.razor"); + var appRazorText = await File.ReadAllTextAsync(appRazorPath); + appRazorText = appRazorText.Replace("IComponentRenderMode?", "IComponentRenderMode").Replace("? null", "? null!"); + await File.WriteAllTextAsync(appRazorPath, appRazorText); + } + + private class BlazorTemplatePages + { + internal const string Index = ""; + internal const string Weather = "weather"; + internal const string Counter = "counter"; + internal const string Auth = "auth"; + internal const string Login = "Account/Login"; + internal const string Register = "Account/Register"; + internal const string ForgotPassword = "Account/ForgotPassword"; + internal const string ResendEmailConfirmation = "Account/ResendEmailConfirmation"; + } +} diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 157b1a01cc6b..7904cc203e19 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -510,11 +510,7 @@ "Files": [ "appsettings.Development.json", "appsettings.json", - "{ProjectName}.csproj", - "Program.cs", "Components/App.razor", - "Components/Routes.razor", - "Components/_Imports.razor", "Components/Layout/MainLayout.razor", "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", @@ -522,11 +518,15 @@ "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", + "Components/Routes.razor", + "Components/_Imports.razor", + "Program.cs", "Properties/launchSettings.json", "wwwroot/app.css", - "wwwroot/favicon.png", "wwwroot/bootstrap/bootstrap.min.css", - "wwwroot/bootstrap/bootstrap.min.css.map" + "wwwroot/bootstrap/bootstrap.min.css.map", + "wwwroot/favicon.png", + "{ProjectName}.csproj" ], "AuthOption": "None" }, @@ -534,71 +534,73 @@ "Template": "blazor", "Arguments": "new blazor --interactivity none --auth Individual", "Files": [ - "app.db", "appsettings.Development.json", "appsettings.json", - "{ProjectName}.csproj", - "Program.cs", + "Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "Components/Account/IdentityNoOpEmailSender.cs", + "Components/Account/IdentityRedirectManager.cs", + "Components/Account/IdentityUserAccessor.cs", + "Components/Account/Pages/ConfirmEmail.razor", + "Components/Account/Pages/ConfirmEmailChange.razor", + "Components/Account/Pages/ExternalLogin.razor", + "Components/Account/Pages/ForgotPassword.razor", + "Components/Account/Pages/ForgotPasswordConfirmation.razor", + "Components/Account/Pages/InvalidPasswordReset.razor", + "Components/Account/Pages/InvalidUser.razor", + "Components/Account/Pages/Lockout.razor", + "Components/Account/Pages/Login.razor", + "Components/Account/Pages/LoginWith2fa.razor", + "Components/Account/Pages/LoginWithRecoveryCode.razor", + "Components/Account/Pages/Manage/ChangePassword.razor", + "Components/Account/Pages/Manage/DeletePersonalData.razor", + "Components/Account/Pages/Manage/Disable2fa.razor", + "Components/Account/Pages/Manage/Email.razor", + "Components/Account/Pages/Manage/EnableAuthenticator.razor", + "Components/Account/Pages/Manage/ExternalLogins.razor", + "Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "Components/Account/Pages/Manage/Index.razor", + "Components/Account/Pages/Manage/PersonalData.razor", + "Components/Account/Pages/Manage/ResetAuthenticator.razor", + "Components/Account/Pages/Manage/SetPassword.razor", + "Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "Components/Account/Pages/Manage/_Imports.razor", + "Components/Account/Pages/Register.razor", + "Components/Account/Pages/RegisterConfirmation.razor", + "Components/Account/Pages/ResendEmailConfirmation.razor", + "Components/Account/Pages/ResetPassword.razor", + "Components/Account/Pages/ResetPasswordConfirmation.razor", + "Components/Account/Pages/_Imports.razor", + "Components/Account/Shared/AccountLayout.razor", + "Components/Account/Shared/ExternalLoginPicker.razor", + "Components/Account/Shared/ManageLayout.razor", + "Components/Account/Shared/ManageNavMenu.razor", + "Components/Account/Shared/RedirectToLogin.razor", + "Components/Account/Shared/ShowRecoveryCodes.razor", + "Components/Account/Shared/StatusMessage.razor", "Components/App.razor", - "Components/Routes.razor", - "Components/_Imports.razor", - "Components/Identity/ExternalLoginPicker.razor", - "Components/Identity/LogoutForm.razor", - "Components/Identity/ShowRecoveryCodes.razor", - "Components/Identity/StatusMessage.razor", "Components/Layout/MainLayout.razor", "Components/Layout/MainLayout.razor.css", - "Components/Layout/ManageLayout.razor", - "Components/Layout/ManageNavMenu.razor", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", "Components/Pages/Auth.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", - "Components/Pages/Account/_Imports.razor", - "Components/Pages/Account/ConfirmEmail.razor", - "Components/Pages/Account/ConfirmEmailChange.razor", - "Components/Pages/Account/ExternalLogin.razor", - "Components/Pages/Account/ForgotPassword.razor", - "Components/Pages/Account/ForgotPasswordConfirmation.razor", - "Components/Pages/Account/InvalidPasswordReset.razor", - "Components/Pages/Account/InvalidUser.razor", - "Components/Pages/Account/Lockout.razor", - "Components/Pages/Account/Login.razor", - "Components/Pages/Account/LoginWith2fa.razor", - "Components/Pages/Account/LoginWithRecoveryCode.razor", - "Components/Pages/Account/Register.razor", - "Components/Pages/Account/RegisterConfirmation.razor", - "Components/Pages/Account/ResendEmailConfirmation.razor", - "Components/Pages/Account/ResetPassword.razor", - "Components/Pages/Account/ResetPasswordConfirmation.razor", - "Components/Pages/Account/Manage/_Imports.razor", - "Components/Pages/Account/Manage/ChangePassword.razor", - "Components/Pages/Account/Manage/DeletePersonalData.razor", - "Components/Pages/Account/Manage/Disable2fa.razor", - "Components/Pages/Account/Manage/Email.razor", - "Components/Pages/Account/Manage/EnableAuthenticator.razor", - "Components/Pages/Account/Manage/ExternalLogins.razor", - "Components/Pages/Account/Manage/GenerateRecoveryCodes.razor", - "Components/Pages/Account/Manage/Index.razor", - "Components/Pages/Account/Manage/PersonalData.razor", - "Components/Pages/Account/Manage/ResetAuthenticator.razor", - "Components/Pages/Account/Manage/SetPassword.razor", - "Components/Pages/Account/Manage/TwoFactorAuthentication.razor", + "Components/Routes.razor", + "Components/_Imports.razor", + "Data/app.db", "Data/ApplicationDbContext.cs", "Data/ApplicationUser.cs", - "Data/UserAccessor.cs", "Data/Migrations/00000000000000_CreateIdentitySchema.cs", "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "Data/Migrations/ApplicationDbContextModelSnapshot.cs", - "Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "Identity/IdentityRedirectManager.cs", + "Program.cs", "Properties/launchSettings.json", "wwwroot/app.css", - "wwwroot/favicon.png", "wwwroot/bootstrap/bootstrap.min.css", - "wwwroot/bootstrap/bootstrap.min.css.map" + "wwwroot/bootstrap/bootstrap.min.css.map", + "wwwroot/favicon.png", + "{ProjectName}.csproj" ], "AuthOption": "Individual" }, @@ -633,73 +635,75 @@ "Template": "blazor", "Arguments": "new blazor --auth Individual", "Files": [ - "app.db", "appsettings.Development.json", "appsettings.json", - "{ProjectName}.csproj", - "Program.cs", + "Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "Components/Account/IdentityNoOpEmailSender.cs", + "Components/Account/IdentityRedirectManager.cs", + "Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs", + "Components/Account/IdentityUserAccessor.cs", + "Components/Account/Pages/ConfirmEmail.razor", + "Components/Account/Pages/ConfirmEmailChange.razor", + "Components/Account/Pages/ExternalLogin.razor", + "Components/Account/Pages/ForgotPassword.razor", + "Components/Account/Pages/ForgotPasswordConfirmation.razor", + "Components/Account/Pages/InvalidPasswordReset.razor", + "Components/Account/Pages/InvalidUser.razor", + "Components/Account/Pages/Lockout.razor", + "Components/Account/Pages/Login.razor", + "Components/Account/Pages/LoginWith2fa.razor", + "Components/Account/Pages/LoginWithRecoveryCode.razor", + "Components/Account/Pages/Manage/ChangePassword.razor", + "Components/Account/Pages/Manage/DeletePersonalData.razor", + "Components/Account/Pages/Manage/Disable2fa.razor", + "Components/Account/Pages/Manage/Email.razor", + "Components/Account/Pages/Manage/EnableAuthenticator.razor", + "Components/Account/Pages/Manage/ExternalLogins.razor", + "Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "Components/Account/Pages/Manage/Index.razor", + "Components/Account/Pages/Manage/PersonalData.razor", + "Components/Account/Pages/Manage/ResetAuthenticator.razor", + "Components/Account/Pages/Manage/SetPassword.razor", + "Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "Components/Account/Pages/Manage/_Imports.razor", + "Components/Account/Pages/Register.razor", + "Components/Account/Pages/RegisterConfirmation.razor", + "Components/Account/Pages/ResendEmailConfirmation.razor", + "Components/Account/Pages/ResetPassword.razor", + "Components/Account/Pages/ResetPasswordConfirmation.razor", + "Components/Account/Pages/_Imports.razor", + "Components/Account/Shared/AccountLayout.razor", + "Components/Account/Shared/ExternalLoginPicker.razor", + "Components/Account/Shared/ManageLayout.razor", + "Components/Account/Shared/ManageNavMenu.razor", + "Components/Account/Shared/RedirectToLogin.razor", + "Components/Account/Shared/ShowRecoveryCodes.razor", + "Components/Account/Shared/StatusMessage.razor", "Components/App.razor", - "Components/Routes.razor", - "Components/_Imports.razor", - "Components/Identity/ExternalLoginPicker.razor", - "Components/Identity/LogoutForm.razor", - "Components/Identity/ShowRecoveryCodes.razor", - "Components/Identity/StatusMessage.razor", "Components/Layout/MainLayout.razor", "Components/Layout/MainLayout.razor.css", - "Components/Layout/ManageLayout.razor", - "Components/Layout/ManageNavMenu.razor", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", "Components/Pages/Auth.razor", - "Components/Pages/Error.razor", "Components/Pages/Counter.razor", + "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", - "Components/Pages/Account/_Imports.razor", - "Components/Pages/Account/ConfirmEmail.razor", - "Components/Pages/Account/ConfirmEmailChange.razor", - "Components/Pages/Account/ExternalLogin.razor", - "Components/Pages/Account/ForgotPassword.razor", - "Components/Pages/Account/ForgotPasswordConfirmation.razor", - "Components/Pages/Account/InvalidPasswordReset.razor", - "Components/Pages/Account/InvalidUser.razor", - "Components/Pages/Account/Lockout.razor", - "Components/Pages/Account/Login.razor", - "Components/Pages/Account/LoginWith2fa.razor", - "Components/Pages/Account/LoginWithRecoveryCode.razor", - "Components/Pages/Account/Register.razor", - "Components/Pages/Account/RegisterConfirmation.razor", - "Components/Pages/Account/ResendEmailConfirmation.razor", - "Components/Pages/Account/ResetPassword.razor", - "Components/Pages/Account/ResetPasswordConfirmation.razor", - "Components/Pages/Account/Manage/_Imports.razor", - "Components/Pages/Account/Manage/ChangePassword.razor", - "Components/Pages/Account/Manage/DeletePersonalData.razor", - "Components/Pages/Account/Manage/Disable2fa.razor", - "Components/Pages/Account/Manage/Email.razor", - "Components/Pages/Account/Manage/EnableAuthenticator.razor", - "Components/Pages/Account/Manage/ExternalLogins.razor", - "Components/Pages/Account/Manage/GenerateRecoveryCodes.razor", - "Components/Pages/Account/Manage/Index.razor", - "Components/Pages/Account/Manage/PersonalData.razor", - "Components/Pages/Account/Manage/ResetAuthenticator.razor", - "Components/Pages/Account/Manage/SetPassword.razor", - "Components/Pages/Account/Manage/TwoFactorAuthentication.razor", + "Components/Routes.razor", + "Components/_Imports.razor", + "Data/app.db", "Data/ApplicationDbContext.cs", "Data/ApplicationUser.cs", - "Data/UserAccessor.cs", "Data/Migrations/00000000000000_CreateIdentitySchema.cs", "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "Data/Migrations/ApplicationDbContextModelSnapshot.cs", - "Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "Identity/IdentityRedirectManager.cs", - "Identity/IdentityRevalidatingAuthenticationStateProvider.cs", + "Program.cs", "Properties/launchSettings.json", "wwwroot/app.css", - "wwwroot/favicon.png", "wwwroot/bootstrap/bootstrap.min.css", - "wwwroot/bootstrap/bootstrap.min.css.map" + "wwwroot/bootstrap/bootstrap.min.css.map", + "wwwroot/favicon.png", + "{ProjectName}.csproj" ], "AuthOption": "Individual" }, @@ -709,70 +713,72 @@ "Files": [ "appsettings.Development.json", "appsettings.json", - "{ProjectName}.csproj", - "Program.cs", + "Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "Components/Account/IdentityNoOpEmailSender.cs", + "Components/Account/IdentityRedirectManager.cs", + "Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs", + "Components/Account/IdentityUserAccessor.cs", + "Components/Account/Pages/ConfirmEmail.razor", + "Components/Account/Pages/ConfirmEmailChange.razor", + "Components/Account/Pages/ExternalLogin.razor", + "Components/Account/Pages/ForgotPassword.razor", + "Components/Account/Pages/ForgotPasswordConfirmation.razor", + "Components/Account/Pages/InvalidPasswordReset.razor", + "Components/Account/Pages/InvalidUser.razor", + "Components/Account/Pages/Lockout.razor", + "Components/Account/Pages/Login.razor", + "Components/Account/Pages/LoginWith2fa.razor", + "Components/Account/Pages/LoginWithRecoveryCode.razor", + "Components/Account/Pages/Manage/ChangePassword.razor", + "Components/Account/Pages/Manage/DeletePersonalData.razor", + "Components/Account/Pages/Manage/Disable2fa.razor", + "Components/Account/Pages/Manage/Email.razor", + "Components/Account/Pages/Manage/EnableAuthenticator.razor", + "Components/Account/Pages/Manage/ExternalLogins.razor", + "Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "Components/Account/Pages/Manage/Index.razor", + "Components/Account/Pages/Manage/PersonalData.razor", + "Components/Account/Pages/Manage/ResetAuthenticator.razor", + "Components/Account/Pages/Manage/SetPassword.razor", + "Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "Components/Account/Pages/Manage/_Imports.razor", + "Components/Account/Pages/Register.razor", + "Components/Account/Pages/RegisterConfirmation.razor", + "Components/Account/Pages/ResendEmailConfirmation.razor", + "Components/Account/Pages/ResetPassword.razor", + "Components/Account/Pages/ResetPasswordConfirmation.razor", + "Components/Account/Pages/_Imports.razor", + "Components/Account/Shared/AccountLayout.razor", + "Components/Account/Shared/ExternalLoginPicker.razor", + "Components/Account/Shared/ManageLayout.razor", + "Components/Account/Shared/ManageNavMenu.razor", + "Components/Account/Shared/RedirectToLogin.razor", + "Components/Account/Shared/ShowRecoveryCodes.razor", + "Components/Account/Shared/StatusMessage.razor", "Components/App.razor", - "Components/Routes.razor", - "Components/_Imports.razor", - "Components/Identity/ExternalLoginPicker.razor", - "Components/Identity/LogoutForm.razor", - "Components/Identity/ShowRecoveryCodes.razor", - "Components/Identity/StatusMessage.razor", "Components/Layout/MainLayout.razor", "Components/Layout/MainLayout.razor.css", - "Components/Layout/ManageLayout.razor", - "Components/Layout/ManageNavMenu.razor", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", "Components/Pages/Auth.razor", - "Components/Pages/Error.razor", "Components/Pages/Counter.razor", + "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", - "Components/Pages/Account/_Imports.razor", - "Components/Pages/Account/ConfirmEmail.razor", - "Components/Pages/Account/ConfirmEmailChange.razor", - "Components/Pages/Account/ExternalLogin.razor", - "Components/Pages/Account/ForgotPassword.razor", - "Components/Pages/Account/ForgotPasswordConfirmation.razor", - "Components/Pages/Account/InvalidPasswordReset.razor", - "Components/Pages/Account/InvalidUser.razor", - "Components/Pages/Account/Lockout.razor", - "Components/Pages/Account/Login.razor", - "Components/Pages/Account/LoginWith2fa.razor", - "Components/Pages/Account/LoginWithRecoveryCode.razor", - "Components/Pages/Account/Register.razor", - "Components/Pages/Account/RegisterConfirmation.razor", - "Components/Pages/Account/ResendEmailConfirmation.razor", - "Components/Pages/Account/ResetPassword.razor", - "Components/Pages/Account/ResetPasswordConfirmation.razor", - "Components/Pages/Account/Manage/_Imports.razor", - "Components/Pages/Account/Manage/ChangePassword.razor", - "Components/Pages/Account/Manage/DeletePersonalData.razor", - "Components/Pages/Account/Manage/Disable2fa.razor", - "Components/Pages/Account/Manage/Email.razor", - "Components/Pages/Account/Manage/EnableAuthenticator.razor", - "Components/Pages/Account/Manage/ExternalLogins.razor", - "Components/Pages/Account/Manage/GenerateRecoveryCodes.razor", - "Components/Pages/Account/Manage/Index.razor", - "Components/Pages/Account/Manage/PersonalData.razor", - "Components/Pages/Account/Manage/ResetAuthenticator.razor", - "Components/Pages/Account/Manage/SetPassword.razor", - "Components/Pages/Account/Manage/TwoFactorAuthentication.razor", + "Components/Routes.razor", + "Components/_Imports.razor", "Data/ApplicationDbContext.cs", "Data/ApplicationUser.cs", - "Data/UserAccessor.cs", "Data/Migrations/00000000000000_CreateIdentitySchema.cs", "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "Data/Migrations/ApplicationDbContextModelSnapshot.cs", - "Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "Identity/IdentityRedirectManager.cs", - "Identity/IdentityRevalidatingAuthenticationStateProvider.cs", + "Program.cs", "Properties/launchSettings.json", "wwwroot/app.css", - "wwwroot/favicon.png", "wwwroot/bootstrap/bootstrap.min.css", - "wwwroot/bootstrap/bootstrap.min.css.map" + "wwwroot/bootstrap/bootstrap.min.css.map", + "wwwroot/favicon.png", + "{ProjectName}.csproj" ], "AuthOption": "None" }, @@ -813,81 +819,83 @@ "Template": "blazor", "Arguments": "new blazor --interactivity webassembly --auth Individual", "Files": [ + "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.Client/Pages/Auth.razor", + "{ProjectName}.Client/Pages/Counter.razor", + "{ProjectName}.Client/PersistentAuthenticationStateProvider.cs", + "{ProjectName}.Client/Program.cs", + "{ProjectName}.Client/RedirectToLogin.razor", + "{ProjectName}.Client/UserInfo.cs", + "{ProjectName}.Client/wwwroot/appsettings.Development.json", + "{ProjectName}.Client/wwwroot/appsettings.json", + "{ProjectName}.Client/_Imports.razor", "{ProjectName}.sln", - "{ProjectName}/app.db", "{ProjectName}/appsettings.Development.json", "{ProjectName}/appsettings.json", - "{ProjectName}/{ProjectName}.csproj", - "{ProjectName}/Program.cs", + "{ProjectName}/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "{ProjectName}/Components/Account/IdentityNoOpEmailSender.cs", + "{ProjectName}/Components/Account/IdentityRedirectManager.cs", + "{ProjectName}/Components/Account/IdentityUserAccessor.cs", + "{ProjectName}/Components/Account/Pages/ConfirmEmail.razor", + "{ProjectName}/Components/Account/Pages/ConfirmEmailChange.razor", + "{ProjectName}/Components/Account/Pages/ExternalLogin.razor", + "{ProjectName}/Components/Account/Pages/ForgotPassword.razor", + "{ProjectName}/Components/Account/Pages/ForgotPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/InvalidPasswordReset.razor", + "{ProjectName}/Components/Account/Pages/InvalidUser.razor", + "{ProjectName}/Components/Account/Pages/Lockout.razor", + "{ProjectName}/Components/Account/Pages/Login.razor", + "{ProjectName}/Components/Account/Pages/LoginWith2fa.razor", + "{ProjectName}/Components/Account/Pages/LoginWithRecoveryCode.razor", + "{ProjectName}/Components/Account/Pages/Manage/ChangePassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/DeletePersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/Disable2fa.razor", + "{ProjectName}/Components/Account/Pages/Manage/Email.razor", + "{ProjectName}/Components/Account/Pages/Manage/EnableAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/ExternalLogins.razor", + "{ProjectName}/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "{ProjectName}/Components/Account/Pages/Manage/Index.razor", + "{ProjectName}/Components/Account/Pages/Manage/PersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/ResetAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/SetPassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Account/Pages/Manage/_Imports.razor", + "{ProjectName}/Components/Account/Pages/Register.razor", + "{ProjectName}/Components/Account/Pages/RegisterConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResendEmailConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResetPassword.razor", + "{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/_Imports.razor", + "{ProjectName}/Components/Account/PersistingServerAuthenticationStateProvider.cs", + "{ProjectName}/Components/Account/Shared/AccountLayout.razor", + "{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor", + "{ProjectName}/Components/Account/Shared/ManageLayout.razor", + "{ProjectName}/Components/Account/Shared/ManageNavMenu.razor", + "{ProjectName}/Components/Account/Shared/ShowRecoveryCodes.razor", + "{ProjectName}/Components/Account/Shared/StatusMessage.razor", "{ProjectName}/Components/App.razor", - "{ProjectName}/Components/Routes.razor", - "{ProjectName}/Components/_Imports.razor", - "{ProjectName}/Components/Identity/ExternalLoginPicker.razor", - "{ProjectName}/Components/Identity/LogoutForm.razor", - "{ProjectName}/Components/Identity/ShowRecoveryCodes.razor", - "{ProjectName}/Components/Identity/StatusMessage.razor", "{ProjectName}/Components/Layout/MainLayout.razor", "{ProjectName}/Components/Layout/MainLayout.razor.css", - "{ProjectName}/Components/Layout/ManageLayout.razor", - "{ProjectName}/Components/Layout/ManageNavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/Weather.razor", - "{ProjectName}/Components/Pages/Account/_Imports.razor", - "{ProjectName}/Components/Pages/Account/ConfirmEmail.razor", - "{ProjectName}/Components/Pages/Account/ConfirmEmailChange.razor", - "{ProjectName}/Components/Pages/Account/ExternalLogin.razor", - "{ProjectName}/Components/Pages/Account/ForgotPassword.razor", - "{ProjectName}/Components/Pages/Account/ForgotPasswordConfirmation.razor", - "{ProjectName}/Components/Pages/Account/InvalidPasswordReset.razor", - "{ProjectName}/Components/Pages/Account/InvalidUser.razor", - "{ProjectName}/Components/Pages/Account/Lockout.razor", - "{ProjectName}/Components/Pages/Account/Login.razor", - "{ProjectName}/Components/Pages/Account/LoginWith2fa.razor", - "{ProjectName}/Components/Pages/Account/LoginWithRecoveryCode.razor", - "{ProjectName}/Components/Pages/Account/Register.razor", - "{ProjectName}/Components/Pages/Account/RegisterConfirmation.razor", - "{ProjectName}/Components/Pages/Account/ResendEmailConfirmation.razor", - "{ProjectName}/Components/Pages/Account/ResetPassword.razor", - "{ProjectName}/Components/Pages/Account/ResetPasswordConfirmation.razor", - "{ProjectName}/Components/Pages/Account/Manage/_Imports.razor", - "{ProjectName}/Components/Pages/Account/Manage/ChangePassword.razor", - "{ProjectName}/Components/Pages/Account/Manage/DeletePersonalData.razor", - "{ProjectName}/Components/Pages/Account/Manage/Disable2fa.razor", - "{ProjectName}/Components/Pages/Account/Manage/Email.razor", - "{ProjectName}/Components/Pages/Account/Manage/EnableAuthenticator.razor", - "{ProjectName}/Components/Pages/Account/Manage/ExternalLogins.razor", - "{ProjectName}/Components/Pages/Account/Manage/GenerateRecoveryCodes.razor", - "{ProjectName}/Components/Pages/Account/Manage/Index.razor", - "{ProjectName}/Components/Pages/Account/Manage/PersonalData.razor", - "{ProjectName}/Components/Pages/Account/Manage/ResetAuthenticator.razor", - "{ProjectName}/Components/Pages/Account/Manage/SetPassword.razor", - "{ProjectName}/Components/Pages/Account/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Routes.razor", + "{ProjectName}/Components/_Imports.razor", + "{ProjectName}/Data/app.db", "{ProjectName}/Data/ApplicationDbContext.cs", "{ProjectName}/Data/ApplicationUser.cs", - "{ProjectName}/Data/UserAccessor.cs", "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.cs", "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", - "{ProjectName}/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "{ProjectName}/Identity/IdentityRedirectManager.cs", - "{ProjectName}/Identity/PersistingServerAuthenticationStateProvider.cs", + "{ProjectName}/{ProjectName}.csproj", + "{ProjectName}/Program.cs", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", - "{ProjectName}/wwwroot/favicon.png", "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css", "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css.map", - "{ProjectName}.Client/{ProjectName}.Client.csproj", - "{ProjectName}.Client/PersistentAuthenticationStateProvider.cs", - "{ProjectName}.Client/Program.cs", - "{ProjectName}.Client/UserInfo.cs", - "{ProjectName}.Client/_Imports.razor", - "{ProjectName}.Client/Pages/Auth.razor", - "{ProjectName}.Client/Pages/Counter.razor", - "{ProjectName}.Client/wwwroot/appsettings.Development.json", - "{ProjectName}.Client/wwwroot/appsettings.json" + "{ProjectName}/wwwroot/favicon.png" ], "AuthOption": "Individual" }, @@ -928,81 +936,83 @@ "Template": "blazor", "Arguments": "new blazor --interactivity auto --auth Individual", "Files": [ + "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.Client/Pages/Auth.razor", + "{ProjectName}.Client/Pages/Counter.razor", + "{ProjectName}.Client/PersistentAuthenticationStateProvider.cs", + "{ProjectName}.Client/Program.cs", + "{ProjectName}.Client/RedirectToLogin.razor", + "{ProjectName}.Client/UserInfo.cs", + "{ProjectName}.Client/wwwroot/appsettings.Development.json", + "{ProjectName}.Client/wwwroot/appsettings.json", + "{ProjectName}.Client/_Imports.razor", "{ProjectName}.sln", - "{ProjectName}/app.db", "{ProjectName}/appsettings.Development.json", "{ProjectName}/appsettings.json", - "{ProjectName}/{ProjectName}.csproj", - "{ProjectName}/Program.cs", + "{ProjectName}/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "{ProjectName}/Components/Account/IdentityNoOpEmailSender.cs", + "{ProjectName}/Components/Account/IdentityRedirectManager.cs", + "{ProjectName}/Components/Account/IdentityUserAccessor.cs", + "{ProjectName}/Components/Account/Pages/ConfirmEmail.razor", + "{ProjectName}/Components/Account/Pages/ConfirmEmailChange.razor", + "{ProjectName}/Components/Account/Pages/ExternalLogin.razor", + "{ProjectName}/Components/Account/Pages/ForgotPassword.razor", + "{ProjectName}/Components/Account/Pages/ForgotPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/InvalidPasswordReset.razor", + "{ProjectName}/Components/Account/Pages/InvalidUser.razor", + "{ProjectName}/Components/Account/Pages/Lockout.razor", + "{ProjectName}/Components/Account/Pages/Login.razor", + "{ProjectName}/Components/Account/Pages/LoginWith2fa.razor", + "{ProjectName}/Components/Account/Pages/LoginWithRecoveryCode.razor", + "{ProjectName}/Components/Account/Pages/Manage/ChangePassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/DeletePersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/Disable2fa.razor", + "{ProjectName}/Components/Account/Pages/Manage/Email.razor", + "{ProjectName}/Components/Account/Pages/Manage/EnableAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/ExternalLogins.razor", + "{ProjectName}/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "{ProjectName}/Components/Account/Pages/Manage/Index.razor", + "{ProjectName}/Components/Account/Pages/Manage/PersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/ResetAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/SetPassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Account/Pages/Manage/_Imports.razor", + "{ProjectName}/Components/Account/Pages/Register.razor", + "{ProjectName}/Components/Account/Pages/RegisterConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResendEmailConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResetPassword.razor", + "{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/_Imports.razor", + "{ProjectName}/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs", + "{ProjectName}/Components/Account/Shared/AccountLayout.razor", + "{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor", + "{ProjectName}/Components/Account/Shared/ManageLayout.razor", + "{ProjectName}/Components/Account/Shared/ManageNavMenu.razor", + "{ProjectName}/Components/Account/Shared/ShowRecoveryCodes.razor", + "{ProjectName}/Components/Account/Shared/StatusMessage.razor", "{ProjectName}/Components/App.razor", - "{ProjectName}/Components/Routes.razor", - "{ProjectName}/Components/_Imports.razor", - "{ProjectName}/Components/Identity/ExternalLoginPicker.razor", - "{ProjectName}/Components/Identity/LogoutForm.razor", - "{ProjectName}/Components/Identity/ShowRecoveryCodes.razor", - "{ProjectName}/Components/Identity/StatusMessage.razor", "{ProjectName}/Components/Layout/MainLayout.razor", "{ProjectName}/Components/Layout/MainLayout.razor.css", - "{ProjectName}/Components/Layout/ManageLayout.razor", - "{ProjectName}/Components/Layout/ManageNavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/Weather.razor", - "{ProjectName}/Components/Pages/Account/_Imports.razor", - "{ProjectName}/Components/Pages/Account/ConfirmEmail.razor", - "{ProjectName}/Components/Pages/Account/ConfirmEmailChange.razor", - "{ProjectName}/Components/Pages/Account/ExternalLogin.razor", - "{ProjectName}/Components/Pages/Account/ForgotPassword.razor", - "{ProjectName}/Components/Pages/Account/ForgotPasswordConfirmation.razor", - "{ProjectName}/Components/Pages/Account/InvalidPasswordReset.razor", - "{ProjectName}/Components/Pages/Account/InvalidUser.razor", - "{ProjectName}/Components/Pages/Account/Lockout.razor", - "{ProjectName}/Components/Pages/Account/Login.razor", - "{ProjectName}/Components/Pages/Account/LoginWith2fa.razor", - "{ProjectName}/Components/Pages/Account/LoginWithRecoveryCode.razor", - "{ProjectName}/Components/Pages/Account/Register.razor", - "{ProjectName}/Components/Pages/Account/RegisterConfirmation.razor", - "{ProjectName}/Components/Pages/Account/ResendEmailConfirmation.razor", - "{ProjectName}/Components/Pages/Account/ResetPassword.razor", - "{ProjectName}/Components/Pages/Account/ResetPasswordConfirmation.razor", - "{ProjectName}/Components/Pages/Account/Manage/_Imports.razor", - "{ProjectName}/Components/Pages/Account/Manage/ChangePassword.razor", - "{ProjectName}/Components/Pages/Account/Manage/DeletePersonalData.razor", - "{ProjectName}/Components/Pages/Account/Manage/Disable2fa.razor", - "{ProjectName}/Components/Pages/Account/Manage/Email.razor", - "{ProjectName}/Components/Pages/Account/Manage/EnableAuthenticator.razor", - "{ProjectName}/Components/Pages/Account/Manage/ExternalLogins.razor", - "{ProjectName}/Components/Pages/Account/Manage/GenerateRecoveryCodes.razor", - "{ProjectName}/Components/Pages/Account/Manage/Index.razor", - "{ProjectName}/Components/Pages/Account/Manage/PersonalData.razor", - "{ProjectName}/Components/Pages/Account/Manage/ResetAuthenticator.razor", - "{ProjectName}/Components/Pages/Account/Manage/SetPassword.razor", - "{ProjectName}/Components/Pages/Account/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Routes.razor", + "{ProjectName}/Components/_Imports.razor", + "{ProjectName}/Data/app.db", "{ProjectName}/Data/ApplicationDbContext.cs", "{ProjectName}/Data/ApplicationUser.cs", - "{ProjectName}/Data/UserAccessor.cs", "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.cs", "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", - "{ProjectName}/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "{ProjectName}/Identity/IdentityRedirectManager.cs", - "{ProjectName}/Identity/PersistingRevalidatingAuthenticationStateProvider.cs", + "{ProjectName}/{ProjectName}.csproj", + "{ProjectName}/Program.cs", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", - "{ProjectName}/wwwroot/favicon.png", "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css", "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css.map", - "{ProjectName}.Client/{ProjectName}.Client.csproj", - "{ProjectName}.Client/PersistentAuthenticationStateProvider.cs", - "{ProjectName}.Client/Program.cs", - "{ProjectName}.Client/UserInfo.cs", - "{ProjectName}.Client/_Imports.razor", - "{ProjectName}.Client/Pages/Auth.razor", - "{ProjectName}.Client/Pages/Counter.razor", - "{ProjectName}.Client/wwwroot/appsettings.Development.json", - "{ProjectName}.Client/wwwroot/appsettings.json" + "{ProjectName}/wwwroot/favicon.png" ], "AuthOption": "Individual" }, @@ -1037,65 +1047,65 @@ "Template": "blazor", "Arguments": "new blazor --interactivity webassembly --all-interactive", "Files": [ - "{ProjectName}/appsettings.Development.json", - "{ProjectName}/appsettings.json", - "{ProjectName}/Components/App.razor", - "{ProjectName}/Components/_Imports.razor", - "{ProjectName}/Program.cs", - "{ProjectName}/Properties/launchSettings.json", - "{ProjectName}/{ProjectName}.csproj", - "{ProjectName}/wwwroot/app.css", - "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css", - "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css.map", - "{ProjectName}/wwwroot/favicon.png", "{ProjectName}.Client/Layout/MainLayout.razor", "{ProjectName}.Client/Layout/MainLayout.razor.css", "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", "{ProjectName}.Client/Pages/Counter.razor", - "{ProjectName}.Client/Pages/Error.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/Weather.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/Routes.razor", - "{ProjectName}.Client/{ProjectName}.Client.csproj", "{ProjectName}.Client/wwwroot/appsettings.Development.json", "{ProjectName}.Client/wwwroot/appsettings.json", "{ProjectName}.Client/_Imports.razor", - "{ProjectName}.sln" - ], - "AuthOption": "None" - }, - "UseServerAndWebAssemblyInteractiveAtRoot": { - "Template": "blazor", - "Arguments": "new blazor --interactivity auto --all-interactive", - "Files": [ + "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.sln", "{ProjectName}/appsettings.Development.json", "{ProjectName}/appsettings.json", "{ProjectName}/Components/App.razor", + "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Program.cs", "{ProjectName}/Properties/launchSettings.json", - "{ProjectName}/{ProjectName}.csproj", "{ProjectName}/wwwroot/app.css", "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css", "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css.map", "{ProjectName}/wwwroot/favicon.png", + "{ProjectName}/{ProjectName}.csproj" + ], + "AuthOption": "None" + }, + "UseServerAndWebAssemblyInteractiveAtRoot": { + "Template": "blazor", + "Arguments": "new blazor --interactivity auto --all-interactive", + "Files": [ "{ProjectName}.Client/Layout/MainLayout.razor", "{ProjectName}.Client/Layout/MainLayout.razor.css", "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", "{ProjectName}.Client/Pages/Counter.razor", - "{ProjectName}.Client/Pages/Error.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/Weather.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/Routes.razor", - "{ProjectName}.Client/{ProjectName}.Client.csproj", "{ProjectName}.Client/wwwroot/appsettings.Development.json", "{ProjectName}.Client/wwwroot/appsettings.json", "{ProjectName}.Client/_Imports.razor", - "{ProjectName}.sln" + "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.sln", + "{ProjectName}/appsettings.Development.json", + "{ProjectName}/appsettings.json", + "{ProjectName}/Components/App.razor", + "{ProjectName}/Components/Pages/Error.razor", + "{ProjectName}/Components/_Imports.razor", + "{ProjectName}/Program.cs", + "{ProjectName}/Properties/launchSettings.json", + "{ProjectName}/wwwroot/app.css", + "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css", + "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css.map", + "{ProjectName}/wwwroot/favicon.png", + "{ProjectName}/{ProjectName}.csproj" ], "AuthOption": "None" }, @@ -1191,71 +1201,317 @@ "Template": "blazor", "Arguments": "new blazor --interactivity auto --empty --auth Individual", "Files": [ + "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.Client/PersistentAuthenticationStateProvider.cs", + "{ProjectName}.Client/Program.cs", + "{ProjectName}.Client/RedirectToLogin.razor", + "{ProjectName}.Client/UserInfo.cs", + "{ProjectName}.Client/_Imports.razor", "{ProjectName}.sln", - "{ProjectName}/app.db", "{ProjectName}/appsettings.Development.json", "{ProjectName}/appsettings.json", - "{ProjectName}/{ProjectName}.csproj", - "{ProjectName}/Program.cs", + "{ProjectName}/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "{ProjectName}/Components/Account/IdentityNoOpEmailSender.cs", + "{ProjectName}/Components/Account/IdentityRedirectManager.cs", + "{ProjectName}/Components/Account/IdentityUserAccessor.cs", + "{ProjectName}/Components/Account/Pages/ConfirmEmail.razor", + "{ProjectName}/Components/Account/Pages/ConfirmEmailChange.razor", + "{ProjectName}/Components/Account/Pages/ExternalLogin.razor", + "{ProjectName}/Components/Account/Pages/ForgotPassword.razor", + "{ProjectName}/Components/Account/Pages/ForgotPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/InvalidPasswordReset.razor", + "{ProjectName}/Components/Account/Pages/InvalidUser.razor", + "{ProjectName}/Components/Account/Pages/Lockout.razor", + "{ProjectName}/Components/Account/Pages/Login.razor", + "{ProjectName}/Components/Account/Pages/LoginWith2fa.razor", + "{ProjectName}/Components/Account/Pages/LoginWithRecoveryCode.razor", + "{ProjectName}/Components/Account/Pages/Manage/ChangePassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/DeletePersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/Disable2fa.razor", + "{ProjectName}/Components/Account/Pages/Manage/Email.razor", + "{ProjectName}/Components/Account/Pages/Manage/EnableAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/ExternalLogins.razor", + "{ProjectName}/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "{ProjectName}/Components/Account/Pages/Manage/Index.razor", + "{ProjectName}/Components/Account/Pages/Manage/PersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/ResetAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/SetPassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Account/Pages/Manage/_Imports.razor", + "{ProjectName}/Components/Account/Pages/Register.razor", + "{ProjectName}/Components/Account/Pages/RegisterConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResendEmailConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResetPassword.razor", + "{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/_Imports.razor", + "{ProjectName}/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs", + "{ProjectName}/Components/Account/Shared/AccountLayout.razor", + "{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor", + "{ProjectName}/Components/Account/Shared/ManageLayout.razor", + "{ProjectName}/Components/Account/Shared/ManageNavMenu.razor", + "{ProjectName}/Components/Account/Shared/ShowRecoveryCodes.razor", + "{ProjectName}/Components/Account/Shared/StatusMessage.razor", "{ProjectName}/Components/App.razor", - "{ProjectName}/Components/Routes.razor", - "{ProjectName}/Components/_Imports.razor", - "{ProjectName}/Components/Identity/ExternalLoginPicker.razor", - "{ProjectName}/Components/Identity/LogoutForm.razor", - "{ProjectName}/Components/Identity/ShowRecoveryCodes.razor", - "{ProjectName}/Components/Identity/StatusMessage.razor", "{ProjectName}/Components/Layout/MainLayout.razor", "{ProjectName}/Components/Layout/MainLayout.razor.css", - "{ProjectName}/Components/Layout/ManageLayout.razor", - "{ProjectName}/Components/Layout/ManageNavMenu.razor", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", - "{ProjectName}/Components/Pages/Account/_Imports.razor", - "{ProjectName}/Components/Pages/Account/ConfirmEmail.razor", - "{ProjectName}/Components/Pages/Account/ConfirmEmailChange.razor", - "{ProjectName}/Components/Pages/Account/ExternalLogin.razor", - "{ProjectName}/Components/Pages/Account/ForgotPassword.razor", - "{ProjectName}/Components/Pages/Account/ForgotPasswordConfirmation.razor", - "{ProjectName}/Components/Pages/Account/InvalidPasswordReset.razor", - "{ProjectName}/Components/Pages/Account/InvalidUser.razor", - "{ProjectName}/Components/Pages/Account/Lockout.razor", - "{ProjectName}/Components/Pages/Account/Login.razor", - "{ProjectName}/Components/Pages/Account/LoginWith2fa.razor", - "{ProjectName}/Components/Pages/Account/LoginWithRecoveryCode.razor", - "{ProjectName}/Components/Pages/Account/Register.razor", - "{ProjectName}/Components/Pages/Account/RegisterConfirmation.razor", - "{ProjectName}/Components/Pages/Account/ResendEmailConfirmation.razor", - "{ProjectName}/Components/Pages/Account/ResetPassword.razor", - "{ProjectName}/Components/Pages/Account/ResetPasswordConfirmation.razor", - "{ProjectName}/Components/Pages/Account/Manage/_Imports.razor", - "{ProjectName}/Components/Pages/Account/Manage/ChangePassword.razor", - "{ProjectName}/Components/Pages/Account/Manage/DeletePersonalData.razor", - "{ProjectName}/Components/Pages/Account/Manage/Disable2fa.razor", - "{ProjectName}/Components/Pages/Account/Manage/Email.razor", - "{ProjectName}/Components/Pages/Account/Manage/EnableAuthenticator.razor", - "{ProjectName}/Components/Pages/Account/Manage/ExternalLogins.razor", - "{ProjectName}/Components/Pages/Account/Manage/GenerateRecoveryCodes.razor", - "{ProjectName}/Components/Pages/Account/Manage/Index.razor", - "{ProjectName}/Components/Pages/Account/Manage/PersonalData.razor", - "{ProjectName}/Components/Pages/Account/Manage/ResetAuthenticator.razor", - "{ProjectName}/Components/Pages/Account/Manage/SetPassword.razor", - "{ProjectName}/Components/Pages/Account/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Routes.razor", + "{ProjectName}/Components/_Imports.razor", + "{ProjectName}/Data/app.db", "{ProjectName}/Data/ApplicationDbContext.cs", "{ProjectName}/Data/ApplicationUser.cs", - "{ProjectName}/Data/UserAccessor.cs", "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.cs", "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", - "{ProjectName}/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "{ProjectName}/Identity/IdentityRedirectManager.cs", - "{ProjectName}/Identity/PersistingRevalidatingAuthenticationStateProvider.cs", + "{ProjectName}/{ProjectName}.csproj", + "{ProjectName}/Program.cs", + "{ProjectName}/Properties/launchSettings.json", + "{ProjectName}/wwwroot/app.css" + ], + "AuthOption": "Individual" + }, + "ServerInteractiveAtRootWithIndividualAuth": { + "Template": "blazor", + "Arguments": "new blazor --all-interactive --auth Individual", + "Files": [ + "appsettings.Development.json", + "appsettings.json", + "Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "Components/Account/IdentityNoOpEmailSender.cs", + "Components/Account/IdentityRedirectManager.cs", + "Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs", + "Components/Account/IdentityUserAccessor.cs", + "Components/Account/Pages/ConfirmEmail.razor", + "Components/Account/Pages/ConfirmEmailChange.razor", + "Components/Account/Pages/ExternalLogin.razor", + "Components/Account/Pages/ForgotPassword.razor", + "Components/Account/Pages/ForgotPasswordConfirmation.razor", + "Components/Account/Pages/InvalidPasswordReset.razor", + "Components/Account/Pages/InvalidUser.razor", + "Components/Account/Pages/Lockout.razor", + "Components/Account/Pages/Login.razor", + "Components/Account/Pages/LoginWith2fa.razor", + "Components/Account/Pages/LoginWithRecoveryCode.razor", + "Components/Account/Pages/Manage/ChangePassword.razor", + "Components/Account/Pages/Manage/DeletePersonalData.razor", + "Components/Account/Pages/Manage/Disable2fa.razor", + "Components/Account/Pages/Manage/Email.razor", + "Components/Account/Pages/Manage/EnableAuthenticator.razor", + "Components/Account/Pages/Manage/ExternalLogins.razor", + "Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "Components/Account/Pages/Manage/Index.razor", + "Components/Account/Pages/Manage/PersonalData.razor", + "Components/Account/Pages/Manage/ResetAuthenticator.razor", + "Components/Account/Pages/Manage/SetPassword.razor", + "Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "Components/Account/Pages/Manage/_Imports.razor", + "Components/Account/Pages/Register.razor", + "Components/Account/Pages/RegisterConfirmation.razor", + "Components/Account/Pages/ResendEmailConfirmation.razor", + "Components/Account/Pages/ResetPassword.razor", + "Components/Account/Pages/ResetPasswordConfirmation.razor", + "Components/Account/Pages/_Imports.razor", + "Components/Account/Shared/AccountLayout.razor", + "Components/Account/Shared/ExternalLoginPicker.razor", + "Components/Account/Shared/ManageLayout.razor", + "Components/Account/Shared/ManageNavMenu.razor", + "Components/Account/Shared/RedirectToLogin.razor", + "Components/Account/Shared/ShowRecoveryCodes.razor", + "Components/Account/Shared/StatusMessage.razor", + "Components/App.razor", + "Components/Layout/MainLayout.razor", + "Components/Layout/MainLayout.razor.css", + "Components/Layout/NavMenu.razor", + "Components/Layout/NavMenu.razor.css", + "Components/Pages/Auth.razor", + "Components/Pages/Counter.razor", + "Components/Pages/Error.razor", + "Components/Pages/Home.razor", + "Components/Pages/Weather.razor", + "Components/Routes.razor", + "Components/_Imports.razor", + "Data/app.db", + "Data/ApplicationDbContext.cs", + "Data/ApplicationUser.cs", + "Data/Migrations/00000000000000_CreateIdentitySchema.cs", + "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", + "Data/Migrations/ApplicationDbContextModelSnapshot.cs", + "Program.cs", + "Properties/launchSettings.json", + "wwwroot/app.css", + "wwwroot/bootstrap/bootstrap.min.css", + "wwwroot/bootstrap/bootstrap.min.css.map", + "wwwroot/favicon.png", + "{ProjectName}.csproj" + ], + "AuthOption": "Individual" + }, + "WebAssemblyInteractiveAtRootWithIndividualAuth": { + "Template": "blazor", + "Arguments": "new blazor --interactivity WebAssembly --all-interactive --auth Individual", + "Files": [ + "{ProjectName}.Client/Layout/MainLayout.razor", + "{ProjectName}.Client/Layout/MainLayout.razor.css", + "{ProjectName}.Client/Layout/NavMenu.razor", + "{ProjectName}.Client/Layout/NavMenu.razor.css", + "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.Client/Pages/Auth.razor", + "{ProjectName}.Client/Pages/Counter.razor", + "{ProjectName}.Client/Pages/Home.razor", + "{ProjectName}.Client/Pages/Weather.razor", + "{ProjectName}.Client/PersistentAuthenticationStateProvider.cs", + "{ProjectName}.Client/Program.cs", + "{ProjectName}.Client/RedirectToLogin.razor", + "{ProjectName}.Client/Routes.razor", + "{ProjectName}.Client/UserInfo.cs", + "{ProjectName}.Client/wwwroot/appsettings.Development.json", + "{ProjectName}.Client/wwwroot/appsettings.json", + "{ProjectName}.Client/_Imports.razor", + "{ProjectName}.sln", + "{ProjectName}/appsettings.Development.json", + "{ProjectName}/appsettings.json", + "{ProjectName}/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "{ProjectName}/Components/Account/IdentityNoOpEmailSender.cs", + "{ProjectName}/Components/Account/IdentityRedirectManager.cs", + "{ProjectName}/Components/Account/IdentityUserAccessor.cs", + "{ProjectName}/Components/Account/Pages/ConfirmEmail.razor", + "{ProjectName}/Components/Account/Pages/ConfirmEmailChange.razor", + "{ProjectName}/Components/Account/Pages/ExternalLogin.razor", + "{ProjectName}/Components/Account/Pages/ForgotPassword.razor", + "{ProjectName}/Components/Account/Pages/ForgotPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/InvalidPasswordReset.razor", + "{ProjectName}/Components/Account/Pages/InvalidUser.razor", + "{ProjectName}/Components/Account/Pages/Lockout.razor", + "{ProjectName}/Components/Account/Pages/Login.razor", + "{ProjectName}/Components/Account/Pages/LoginWith2fa.razor", + "{ProjectName}/Components/Account/Pages/LoginWithRecoveryCode.razor", + "{ProjectName}/Components/Account/Pages/Manage/ChangePassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/DeletePersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/Disable2fa.razor", + "{ProjectName}/Components/Account/Pages/Manage/Email.razor", + "{ProjectName}/Components/Account/Pages/Manage/EnableAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/ExternalLogins.razor", + "{ProjectName}/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "{ProjectName}/Components/Account/Pages/Manage/Index.razor", + "{ProjectName}/Components/Account/Pages/Manage/PersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/ResetAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/SetPassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Account/Pages/Manage/_Imports.razor", + "{ProjectName}/Components/Account/Pages/Register.razor", + "{ProjectName}/Components/Account/Pages/RegisterConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResendEmailConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResetPassword.razor", + "{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/_Imports.razor", + "{ProjectName}/Components/Account/PersistingServerAuthenticationStateProvider.cs", + "{ProjectName}/Components/Account/Shared/AccountLayout.razor", + "{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor", + "{ProjectName}/Components/Account/Shared/ManageLayout.razor", + "{ProjectName}/Components/Account/Shared/ManageNavMenu.razor", + "{ProjectName}/Components/Account/Shared/ShowRecoveryCodes.razor", + "{ProjectName}/Components/Account/Shared/StatusMessage.razor", + "{ProjectName}/Components/App.razor", + "{ProjectName}/Components/Pages/Error.razor", + "{ProjectName}/Components/_Imports.razor", + "{ProjectName}/Data/app.db", + "{ProjectName}/Data/ApplicationDbContext.cs", + "{ProjectName}/Data/ApplicationUser.cs", + "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.cs", + "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", + "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", + "{ProjectName}/{ProjectName}.csproj", + "{ProjectName}/Program.cs", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", + "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css", + "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css.map", + "{ProjectName}/wwwroot/favicon.png" + ], + "AuthOption": "Individual" + }, + "AutoInteractiveAtRootWithIndividualAuth": { + "Template": "blazor", + "Arguments": "new blazor --interactivity Auto --all-interactive --auth Individual", + "Files": [ + "{ProjectName}.Client/Layout/MainLayout.razor", + "{ProjectName}.Client/Layout/MainLayout.razor.css", + "{ProjectName}.Client/Layout/NavMenu.razor", + "{ProjectName}.Client/Layout/NavMenu.razor.css", "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.Client/Pages/Auth.razor", + "{ProjectName}.Client/Pages/Counter.razor", + "{ProjectName}.Client/Pages/Home.razor", + "{ProjectName}.Client/Pages/Weather.razor", "{ProjectName}.Client/PersistentAuthenticationStateProvider.cs", "{ProjectName}.Client/Program.cs", + "{ProjectName}.Client/RedirectToLogin.razor", + "{ProjectName}.Client/Routes.razor", "{ProjectName}.Client/UserInfo.cs", - "{ProjectName}.Client/_Imports.razor" + "{ProjectName}.Client/wwwroot/appsettings.Development.json", + "{ProjectName}.Client/wwwroot/appsettings.json", + "{ProjectName}.Client/_Imports.razor", + "{ProjectName}.sln", + "{ProjectName}/appsettings.Development.json", + "{ProjectName}/appsettings.json", + "{ProjectName}/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs", + "{ProjectName}/Components/Account/IdentityNoOpEmailSender.cs", + "{ProjectName}/Components/Account/IdentityRedirectManager.cs", + "{ProjectName}/Components/Account/IdentityUserAccessor.cs", + "{ProjectName}/Components/Account/Pages/ConfirmEmail.razor", + "{ProjectName}/Components/Account/Pages/ConfirmEmailChange.razor", + "{ProjectName}/Components/Account/Pages/ExternalLogin.razor", + "{ProjectName}/Components/Account/Pages/ForgotPassword.razor", + "{ProjectName}/Components/Account/Pages/ForgotPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/InvalidPasswordReset.razor", + "{ProjectName}/Components/Account/Pages/InvalidUser.razor", + "{ProjectName}/Components/Account/Pages/Lockout.razor", + "{ProjectName}/Components/Account/Pages/Login.razor", + "{ProjectName}/Components/Account/Pages/LoginWith2fa.razor", + "{ProjectName}/Components/Account/Pages/LoginWithRecoveryCode.razor", + "{ProjectName}/Components/Account/Pages/Manage/ChangePassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/DeletePersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/Disable2fa.razor", + "{ProjectName}/Components/Account/Pages/Manage/Email.razor", + "{ProjectName}/Components/Account/Pages/Manage/EnableAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/ExternalLogins.razor", + "{ProjectName}/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor", + "{ProjectName}/Components/Account/Pages/Manage/Index.razor", + "{ProjectName}/Components/Account/Pages/Manage/PersonalData.razor", + "{ProjectName}/Components/Account/Pages/Manage/ResetAuthenticator.razor", + "{ProjectName}/Components/Account/Pages/Manage/SetPassword.razor", + "{ProjectName}/Components/Account/Pages/Manage/TwoFactorAuthentication.razor", + "{ProjectName}/Components/Account/Pages/Manage/_Imports.razor", + "{ProjectName}/Components/Account/Pages/Register.razor", + "{ProjectName}/Components/Account/Pages/RegisterConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResendEmailConfirmation.razor", + "{ProjectName}/Components/Account/Pages/ResetPassword.razor", + "{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor", + "{ProjectName}/Components/Account/Pages/_Imports.razor", + "{ProjectName}/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs", + "{ProjectName}/Components/Account/Shared/AccountLayout.razor", + "{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor", + "{ProjectName}/Components/Account/Shared/ManageLayout.razor", + "{ProjectName}/Components/Account/Shared/ManageNavMenu.razor", + "{ProjectName}/Components/Account/Shared/ShowRecoveryCodes.razor", + "{ProjectName}/Components/Account/Shared/StatusMessage.razor", + "{ProjectName}/Components/App.razor", + "{ProjectName}/Components/Pages/Error.razor", + "{ProjectName}/Components/_Imports.razor", + "{ProjectName}/Data/app.db", + "{ProjectName}/Data/ApplicationDbContext.cs", + "{ProjectName}/Data/ApplicationUser.cs", + "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.cs", + "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", + "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", + "{ProjectName}/{ProjectName}.csproj", + "{ProjectName}/Program.cs", + "{ProjectName}/Properties/launchSettings.json", + "{ProjectName}/wwwroot/app.css", + "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css", + "{ProjectName}/wwwroot/bootstrap/bootstrap.min.css.map", + "{ProjectName}/wwwroot/favicon.png" ], "AuthOption": "Individual" } diff --git a/src/Shared/Process/ProcessEx.cs b/src/Shared/Process/ProcessEx.cs index 44e91b23ca3b..41adb92aee92 100644 --- a/src/Shared/Process/ProcessEx.cs +++ b/src/Shared/Process/ProcessEx.cs @@ -49,6 +49,11 @@ public ProcessEx(ITestOutputHelper output, Process proc, TimeSpan timeout) proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); + if (proc.HasExited) + { + OnProcessExited(); + } + // We greedily create a timeout exception message even though a timeout is unlikely to happen for two reasons: // 1. To make it less likely for Process getters to throw exceptions like "System.InvalidOperationException: Process has exited, ..." // 2. To ensure if/when exceptions are thrown from Process getters, these exceptions can easily be observed. @@ -171,7 +176,7 @@ private void OnOutputData(object sender, DataReceivedEventArgs e) _stdoutLines?.Add(e.Data); } - private void OnProcessExited(object sender, EventArgs e) + private void OnProcessExited(object sender = null, EventArgs e = null) { lock (_testOutputLock) { @@ -182,7 +187,7 @@ private void OnProcessExited(object sender, EventArgs e) } // Don't remove this line - There is a race condition where the process exits and we grab the output before the stdout/stderr completed writing. _process.WaitForExit(); - _stdoutLines.CompleteAdding(); + _stdoutLines?.CompleteAdding(); _stdoutLines = null; _exited.TrySetResult(_process.ExitCode); }