From 10e7a6f67d5302107778bb8d580c355cfa4431ca Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 14 Sep 2023 17:38:29 -0700 Subject: [PATCH 01/18] Add Identity Components to Blazor template --- src/ProjectTemplates/ProjectTemplates.slnf | 4 +- .../BlazorWeb-CSharp.Client.csproj.in | 1 + .../BlazorWeb-CSharp.csproj.in | 12 +- .../RazorPagesWeb-CSharp.csproj.in | 2 +- .../.template.config/dotnetcli.host.json | 8 +- .../.template.config/ide.host.json | 6 + .../localize/templatestrings.cs.json | 5 + .../localize/templatestrings.de.json | 5 + .../localize/templatestrings.en.json | 11 +- .../localize/templatestrings.es.json | 5 + .../localize/templatestrings.fr.json | 5 + .../localize/templatestrings.it.json | 5 + .../localize/templatestrings.ja.json | 5 + .../localize/templatestrings.ko.json | 5 + .../localize/templatestrings.pl.json | 5 + .../localize/templatestrings.pt-BR.json | 5 + .../localize/templatestrings.ru.json | 5 + .../localize/templatestrings.tr.json | 5 + .../localize/templatestrings.zh-Hans.json | 5 + .../localize/templatestrings.zh-Hant.json | 5 + .../.template.config/template.json | 79 ++++- .../BlazorWeb-CSharp.Client/Pages/Auth.razor | 14 + .../PersistedAuthenticationStateProvider.cs | 30 ++ .../BlazorWeb-CSharp.Client/Program.cs | 10 + .../BlazorWeb-CSharp.Client/UserInfo.cs | 7 + .../BlazorWeb-CSharp.Client/_Imports.razor | 3 + .../Components/Layout/ManageLayout.razor | 17 ++ .../Components/Layout/ManageNavMenu.razor | 37 +++ .../Components/Layout/NavMenu.razor | 32 ++ .../Components/Layout/NavMenu.razor.css | 18 ++ .../Pages/Account/ConfirmEmail.razor | 48 +++ .../Pages/Account/ForgotPassword.razor | 72 +++++ .../Account/ForgotPasswordConfirmation.razor | 8 + .../Pages/Account/InvalidPasswordReset.razor | 8 + .../Components/Pages/Account/Login.razor | 147 +++++++++ .../Pages/Account/Manage/ChangePassword.razor | 107 +++++++ .../Pages/Account/Manage/Email.razor | 138 +++++++++ .../Pages/Account/Manage/ExternalLogins.razor | 7 + .../Pages/Account/Manage/Index.razor | 92 ++++++ .../Pages/Account/Manage/PersonalData.razor | 7 + .../Manage/TwoFactorAuthentication.razor | 7 + .../Pages/Account/Manage/_Imports.razor | 2 + .../Components/Pages/Account/Register.razor | 203 +++++++++++++ .../Pages/Account/RegisterConfirmation.razor | 65 ++++ .../Pages/Account/ResetPassword.razor | 107 +++++++ .../Account/ResetPasswordConfirmation.razor | 7 + .../Pages/Account/StatusMessage.razor | 12 + .../BlazorWeb-CSharp/Components/Routes.razor | 4 + .../Components/_Imports.razor | 3 + .../Data/ApplicationDbContext.cs | 12 + .../BlazorWeb-CSharp/Data/ApplicationUser.cs | 9 + ...000000000_CreateIdentitySchema.Designer.cs | 268 +++++++++++++++++ .../00000000000000_CreateIdentitySchema.cs | 222 ++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 265 +++++++++++++++++ ...000000000_CreateIdentitySchema.Designer.cs | 279 ++++++++++++++++++ .../00000000000000_CreateIdentitySchema.cs | 224 ++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 276 +++++++++++++++++ .../Data/UserManagerExtensions.cs | 24 ++ .../PersistingAuthenticationStateProvider.cs | 51 ++++ .../BlazorWeb-CSharp/Program.Main.cs | 34 +++ .../BlazorWeb-CSharp/Program.cs | 38 ++- .../BlazorWeb-CSharp/BlazorWeb-CSharp/app.db | Bin 0 -> 102400 bytes .../BlazorWeb-CSharp/appsettings.json | 9 + .../.template.config/template.json | 1 - .../.template.config/template.json | 3 +- .../.template.config/template.json | 1 - .../.template.config/template.json | 1 - 67 files changed, 3095 insertions(+), 22 deletions(-) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/UserInfo.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageLayout.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageNavMenu.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmail.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPassword.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPasswordConfirmation.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidPasswordReset.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Login.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ChangePassword.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Email.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ExternalLogins.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Index.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/PersonalData.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/TwoFactorAuthentication.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/_Imports.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Register.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/RegisterConfirmation.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPassword.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPasswordConfirmation.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationDbContext.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationUser.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/app.db diff --git a/src/ProjectTemplates/ProjectTemplates.slnf b/src/ProjectTemplates/ProjectTemplates.slnf index 56945fef18d9..a3e1d4b51627 100644 --- a/src/ProjectTemplates/ProjectTemplates.slnf +++ b/src/ProjectTemplates/ProjectTemplates.slnf @@ -65,8 +65,8 @@ "src\\ProjectTemplates\\Web.ItemTemplates\\Microsoft.DotNet.Web.ItemTemplates.csproj", "src\\ProjectTemplates\\Web.ProjectTemplates\\Microsoft.DotNet.Web.ProjectTemplates.csproj", "src\\ProjectTemplates\\test\\Templates.Blazor.Tests\\Templates.Blazor.Tests.csproj", - "src\\ProjectTemplates\\test\\Templates.Blazor.WebAssembly.Tests\\Templates.Blazor.WebAssembly.Tests.csproj", "src\\ProjectTemplates\\test\\Templates.Blazor.WebAssembly.Auth.Tests\\Templates.Blazor.WebAssembly.Auth.Tests.csproj", + "src\\ProjectTemplates\\test\\Templates.Blazor.WebAssembly.Tests\\Templates.Blazor.WebAssembly.Tests.csproj", "src\\ProjectTemplates\\test\\Templates.Mvc.Tests\\Templates.Mvc.Tests.csproj", "src\\ProjectTemplates\\test\\Templates.Tests\\Templates.Tests.csproj", "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", @@ -95,4 +95,4 @@ "src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj" ] } -} +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.Client.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.Client.csproj.in index 027eeefed22b..829c12db913c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.Client.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.Client.csproj.in @@ -12,6 +12,7 @@ + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in index 31be9a845da9..177eabb7b5d2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/BlazorWeb-CSharp.csproj.in @@ -4,15 +4,21 @@ ${DefaultNetCoreTargetFramework} enable enable + aspnet-BlazorWeb-CSharp-53bc9b9d-9d6a-45d4-8429-2a2761773502 True BlazorWeb-CSharp `$(AssemblyName.Replace(' ', '_')) - + - - + + + + + + + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in index ee04cb442ea1..2ed95f454912 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/RazorPagesWeb-CSharp.csproj.in @@ -4,7 +4,7 @@ ${DefaultNetCoreTargetFramework} enable enable - aspnet-Company.WebApplication1-0ce56475-d1db-490f-8af1-a881ea4fcd2d + aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502 True Company.WebApplication1 diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/dotnetcli.host.json index a8c678b039b3..0f065fcebccc 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/dotnetcli.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/dotnetcli.host.json @@ -21,6 +21,9 @@ "IncludeSampleContent": { "isHidden": true }, + "UseLocalDB": { + "longName": "use-local-db" + }, "Framework": { "longName": "framework" }, @@ -51,5 +54,8 @@ "longName": "use-program-main", "shortName": "" } - } + }, + "usageExamples": [ + "--use-wasm --auth Individual --use-local-db" + ] } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/ide.host.json index 5c5a1d00b7f4..97fc244b1c22 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/ide.host.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/ide.host.json @@ -20,6 +20,12 @@ "persistenceScope": "shared", "persistenceScopeName": "Microsoft" }, + { + "id": "auth", + "isVisible": true, + "defaultValue": "None", + "persistenceScope": "templateGroup" + }, { "id": "UseProgramMain", "isVisible": true, diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.cs.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.cs.json index 069f5694009b..65699b857548 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.cs.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.cs.json @@ -4,6 +4,7 @@ "description": "Šablona projektu pro vytvoření webové aplikace Blazor, která podporuje vykreslování na straně serveru i interaktivitu klienta. Tato šablona se dá použít pro webové aplikace s bohatými dynamickými uživatelskými rozhraními (UI).", "symbols/Framework/description": "Cílová architektura pro projekt", "symbols/Framework/choices/net8.0/description": "Cílový net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Pokud se tato možnost zadá, přeskočí automatické obnovení projektu při vytvoření.", "symbols/ExcludeLaunchSettings/description": "Určuje, jestli se má z vygenerované šablony vyloučit soubor launchSettings.json.", "symbols/kestrelHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP v souboru launchSettings.json.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Zahrnout ukázkové stránky", "symbols/IncludeSampleContent/description": "Nastavuje, jestli se mají přidávat ukázkové stránky a styly pro demonstraci základních vzorů použití.", "symbols/Empty/description": "Nastavuje, jestli se mají vynechat ukázkové stránky a styly, které demonstrují základní vzory použití.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Určuje, jestli se má protokol HTTPS vypnout. Tato možnost platí jenom v případě, že se pro --auth nepoužívají Individual, IndividualB2C, SingleOrg ani MultiOrg.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.de.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.de.json index 96805c77240a..e443e2846c5a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.de.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.de.json @@ -4,6 +4,7 @@ "description": "Eine Projektvorlage zum Erstellen einer Blazor-Web-App, die sowohl serverseitiges Rendering als auch Clientinteraktivität unterstützt. Diese Vorlage kann für Web-Apps mit umfangreichen dynamischen Benutzeroberflächen (UIs) verwendet werden.", "symbols/Framework/description": "Das Zielframework für das Projekt.", "symbols/Framework/choices/net8.0/description": "Ziel net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Wenn angegeben, wird die automatische Wiederherstellung des Projekts beim Erstellen übersprungen.", "symbols/ExcludeLaunchSettings/description": "Ob launchSettings.json aus der generierten Vorlage ausgeschlossen werden soll.", "symbols/kestrelHttpPort/description": "Portnummer, die für den HTTP Endpunkt in launchSettings.json verwendet werden soll.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Include Beispielseiten", "symbols/IncludeSampleContent/description": "Konfiguriert, ob Beispielseiten und Stile hinzugefügt werden, um grundlegende Verwendungsmuster zu veranschaulichen.", "symbols/Empty/description": "Konfiguriert, ob Beispielseiten und Formatierungen weggelassen werden sollen, die grundlegende Verwendungsmuster veranschaulichen.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Ob HTTPS deaktiviert werden soll. Diese Option gilt nur, wenn Individual, IndividualB2C, SingleOrg oder MultiOrg nicht für --auth verwendet werden.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.en.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.en.json index 60d16e15ffd9..6db424a9f348 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.en.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.en.json @@ -4,12 +4,13 @@ "description": "A project template for creating a Blazor web app that supports both server-side rendering and client interactivity. This template can be used for web apps with rich dynamic user interfaces (UIs).", "symbols/Framework/description": "The target framework for the project.", "symbols/Framework/choices/net8.0/description": "Target net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", + "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if Individual auth is used).", "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", + "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if Individual auth is used).", "symbols/InteractivityPlatform/displayName": "_Interactivity type", "symbols/InteractivityPlatform/description": "Chooses which hosting platform to use for interactive components", "symbols/InteractivityPlatform/choices/None/displayName": "None", @@ -29,9 +30,13 @@ "symbols/IncludeSampleContent/displayName": "_Include sample pages", "symbols/IncludeSampleContent/description": "Configures whether to add sample pages and styling to demonstrate basic usage patterns.", "symbols/Empty/description": "Configures whether to omit sample pages and styling that demonstrate basic usage patterns.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", + "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual isn't used for --auth.", "symbols/UseProgramMain/displayName": "Do not use _top-level statements", "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", "postActions/restore/description": "Restore NuGet packages required by this project.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.es.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.es.json index 91d5403e0fbf..ddb3601195d3 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.es.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.es.json @@ -4,6 +4,7 @@ "description": "Plantilla de proyecto para crear una aplicación web de Blazor que admita tanto la representación del lado del servidor como la interactividad del cliente. Esta plantilla se puede usar para las aplicaciones web con interfaces de usuario dinámicas enriquecidas.", "symbols/Framework/description": "Marco de destino del proyecto.", "symbols/Framework/choices/net8.0/description": "net8.0 de destino", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Si se especifica, se omite la restauración automática del proyecto durante la creación.", "symbols/ExcludeLaunchSettings/description": "Indica si se va a excluir launchSettings.json de la plantilla generada.", "symbols/kestrelHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP en launchSettings.json.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Incluir páginas de ejemplo", "symbols/IncludeSampleContent/description": "Configura si se van a agregar páginas de ejemplo y estilos para mostrar patrones de uso básicos.", "symbols/Empty/description": "Configura si se omiten las páginas de ejemplo y los estilos que muestran patrones de uso básicos.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Si se va a desactivar HTTPS. Esta opción solo se aplica si Individual, IndividualB2C, SingleOrg o MultiOrg no se usan para --auth.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.fr.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.fr.json index 044f40bacc91..b902e5a58b87 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.fr.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.fr.json @@ -4,6 +4,7 @@ "description": "Modèle de projet pour la création d’une application web Blazor qui prend en charge le rendu côté serveur et l’interactivité du client. Ce modèle peut être utilisé pour les applications web avec des interfaces utilisateur dynamiques enrichies.", "symbols/Framework/description": "Framework cible du projet.", "symbols/Framework/choices/net8.0/description": "Cible net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "S’il est spécifié, ignore la restauration automatique du projet lors de la création.", "symbols/ExcludeLaunchSettings/description": "Indique s’il faut exclure launchSettings.json du modèle généré.", "symbols/kestrelHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP dans launchSettings.json.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Inclure des exemples de pages", "symbols/IncludeSampleContent/description": "Configure s'il faut ajouter des exemples de pages et de style pour illustrer les modèles d'utilisation de base.", "symbols/Empty/description": "Configure s'il faut omettre les exemples de pages et le style qui illustrent les modèles d'utilisation de base.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Indique s’il faut désactiver HTTPS. Cette option s’applique uniquement si Individual, IndividualB2C, SingleOrg ou MultiOrg ne sont pas utilisés pour --auth.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.it.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.it.json index 09c3ebfff2c2..14edbef4335b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.it.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.it.json @@ -4,6 +4,7 @@ "description": "Modello di progetto per la creazione di un'app Web Blazor che supporta sia il rendering lato server sia l'interattività client. Questo modello può essere usato per app Web con interfacce utente dinamiche avanzate.", "symbols/Framework/description": "Il framework di destinazione per il progetto.", "symbols/Framework/choices/net8.0/description": "Destinazione net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Se specificato, ignora il ripristino automatico del progetto durante la creazione.", "symbols/ExcludeLaunchSettings/description": "Indica se escludere launchSettings.json dal modello generato.", "symbols/kestrelHttpPort/description": "Numero di porta da usare per l'endpoint HTTP in launchSettings.json.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Include pagine di esempio", "symbols/IncludeSampleContent/description": "Consente di configurare se aggiungere pagine di esempio e stile per mostrare modelli di utilizzo di base.", "symbols/Empty/description": "Consente di configurare se omettere pagine di esempio e stile che mostrano modelli di utilizzo di base.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Indica se disattivare HTTPS. Questa opzione si applica solo se Individual, IndividualB2C, SingleOrg o MultiOrg non vengono usati per --auth.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ja.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ja.json index f48aecfb9534..2b7fabb722e0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ja.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ja.json @@ -4,6 +4,7 @@ "description": "サーバー側のレンダリングとクライアントの対話機能の両方をサポートする Blazor Web アプリを作成するためのプロジェクト テンプレートです。このテンプレートは、リッチな動的ユーザー インターフェイス (UI) を持つ Web アプリに使用できます。", "symbols/Framework/description": "プロジェクトのターゲット フレームワークです。", "symbols/Framework/choices/net8.0/description": "ターゲット net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "指定した場合、作成時にプロジェクトの自動復元がスキップされます。", "symbols/ExcludeLaunchSettings/description": "生成されたテンプレートから launchSettings.json を除外するかどうか。", "symbols/kestrelHttpPort/description": "launchSettings.json の HTTP エンドポイントに使用するポート番号。", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "サンプル ページを含める(_I)", "symbols/IncludeSampleContent/description": "基本的な使用パターンを示すサンプル ページとスタイルを追加するかどうかを構成します。", "symbols/Empty/description": "基本的な使用パターンを示すサンプル ページとスタイルを省略するかどうかを構成します。", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "HTTPS をオフにするかどうか。このオプションは、Individual、IndividualB2C、SingleOrg、または MultiOrg が --auth に使用されていない場合にのみ適用されます。", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ko.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ko.json index 020ab522da02..f0319af43653 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ko.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ko.json @@ -4,6 +4,7 @@ "description": "서버 측 렌더링 및 클라이언트 대화형 작업을 모두 지원하는 Blazor 웹앱을 만들기 위한 프로젝트 템플릿입니다. 이 템플릿은 풍부한 동적 UI(사용자 인터페이스)가 있는 웹앱에 사용할 수 있습니다.", "symbols/Framework/description": "프로젝트에 대한 대상 프레임워크입니다.", "symbols/Framework/choices/net8.0/description": "대상 net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "지정된 경우, 프로젝트 생성 시 자동 복원을 건너뜁니다.", "symbols/ExcludeLaunchSettings/description": "생성된 템플릿에서 launchSettings.json을 제외할지 여부입니다.", "symbols/kestrelHttpPort/description": "launchSettings.json의 HTTP 엔드포인트에 사용할 포트 번호입니다.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "샘플 페이지 포함(_I)", "symbols/IncludeSampleContent/description": "기본 사용 패턴을 보여주기 위해 샘플 페이지 및 스타일을 추가할지 여부를 구성합니다.", "symbols/Empty/description": "기본 사용 패턴을 보여주는 샘플 페이지 및 스타일을 생략할지 여부를 구성합니다.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "HTTPS를 끌지 여부입니다. 이 옵션은 Individual, IndividualB2C, SingleOrg 또는 MultiOrg가 --auth에 사용되지 않는 경우에만 적용됩니다.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pl.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pl.json index 43e2693e0f62..aaa04bb545d6 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pl.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pl.json @@ -4,6 +4,7 @@ "description": "Szablon projektu służący do tworzenia aplikacji internetowej platformy Blazor, która obsługuje renderowanie po stronie serwera i interakcyjność klienta. Ten szablon może być używany dla aplikacji internetowych z zaawansowanymi dynamicznymi interfejsami użytkownika.", "symbols/Framework/description": "Platforma docelowa dla tego projektu.", "symbols/Framework/choices/net8.0/description": "Docelowa platforma net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Jeśli ta opcja jest określona, pomija automatyczne przywracanie projektu podczas tworzenia.", "symbols/ExcludeLaunchSettings/description": "Określa, czy wykluczyć plik launchSettings.json z wygenerowanego szablonu.", "symbols/kestrelHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP w pliku launchSettings.json.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Dołącz przykładowe strony", "symbols/IncludeSampleContent/description": "Konfiguruje, czy dodać przykładowe strony i style w celu zademonstrowania podstawowych wzorców użycia.", "symbols/Empty/description": "Konfiguruje, czy pomijać przykładowe strony i style demonstrujące podstawowe wzorce użycia.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Określa, czy wyłączyć protokół HTTPS. Ta opcja ma zastosowanie tylko wtedy, gdy dla uwierzytelniania --auth nie są używane elementy Individual, IndividualB2C, SingleOrg lub MultiOrg.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pt-BR.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pt-BR.json index dc3922339c3b..b0969274dcc2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pt-BR.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.pt-BR.json @@ -4,6 +4,7 @@ "description": "Um modelo de projeto para criar um aplicativo Web Blazor que dá suporte à renderização do lado do servidor e à interatividade do cliente. Este modelo pode ser usado para aplicativos da Web com interfaces de usuário (UIs) dinâmicas avançadas.", "symbols/Framework/description": "A estrutura de destino do projeto.", "symbols/Framework/choices/net8.0/description": "net8.0 de destino", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Se especificado, ignora a restauração automática do projeto sendo criado.", "symbols/ExcludeLaunchSettings/description": "Se deve excluir launchSettings.json do modelo gerado.", "symbols/kestrelHttpPort/description": "Número da porta a ser usada para o ponto de extremidade HTTP em launchSettings.json.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Incluir páginas de amostra", "symbols/IncludeSampleContent/description": "Configura se deseja adicionar páginas de amostra e estilo para demonstrar padrões de uso básicos.", "symbols/Empty/description": "Configura a omissão de páginas de amostra e estilo que demonstram padrões básicos de uso.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Se o HTTPS deve ser desativado. Essa opção se aplica somente se Individual, IndividualB2C, SingleOrg ou MultiOrg não forem usados para --auth.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ru.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ru.json index f9d64e65d85a..eea4080639dc 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ru.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.ru.json @@ -4,6 +4,7 @@ "description": "Шаблон проекта для создания приложения Blazor, поддерживающего как отрисовку на стороне сервера, так и интерактивные возможности клиента. Этот шаблон можно использовать для веб-приложений с многофункциональными динамическими пользовательскими интерфейсами (UI).", "symbols/Framework/description": "Целевая платформа для проекта.", "symbols/Framework/choices/net8.0/description": "Целевая net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Если установлено, автоматическое восстановление проекта при создании пропускается.", "symbols/ExcludeLaunchSettings/description": "Следует ли исключить launchSettings.json из созданного шаблона.", "symbols/kestrelHttpPort/description": "Номер порта, используемый для конечной точки HTTP в launchSettings.json.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "_Включить примеры страниц", "symbols/IncludeSampleContent/description": "Настраивает, следует ли добавлять примеры страниц и стили для демонстрации базовых шаблонов использования.", "symbols/Empty/description": "Настраивает, следует ли пропускать примеры страниц и стили, демонстрирующие базовые шаблоны использования.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "Следует ли отключить HTTPS. Этот параметр применяется, только если для --auth не используются Individual, IndividualB2C, SingleOrg или MultiOrg.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.tr.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.tr.json index 5299f026392a..ae3be7fb7b06 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.tr.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.tr.json @@ -4,6 +4,7 @@ "description": "Hem sunucu tarafı işlemeyi hem de istemci etkileşimini destekleyen bir Blazor web uygulaması oluşturmaya yönelik proje şablonu. Bu şablon, zengin dinamik kullanıcı arabirimlerine (UI) sahip web uygulamaları için kullanılabilir.", "symbols/Framework/description": "Projenin hedef çerçevesi.", "symbols/Framework/choices/net8.0/description": "Hedef net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "Belirtilirse, oluşturma sırasında projenin otomatik geri yüklenmesini atlar.", "symbols/ExcludeLaunchSettings/description": "launchSettings.json öğesinin oluşturulan şablondan dışlanıp dışlanmayacağı.", "symbols/kestrelHttpPort/description": "launchSettings.json içinde HTTP uç noktası için kullanılacak bağlantı noktası numarası.", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "Örnek _sayfalar ekle", "symbols/IncludeSampleContent/description": "Temel kullanım düzenlerini göstermek için örnek sayfaların ve stil oluşturma özelliklerinin eklenip eklenmeyeceğini yapılandırır.", "symbols/Empty/description": "Temel kullanım düzenlerini gösteren örnek sayfaların ve stil oluşturma özelliklerinin atlanıp atlanmayacağını yapılandırır.", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "HTTPS'nin kapatılıp kapatılmayacağı. Bu seçenek yalnızca Bireysel, IndividualB2C, SingleOrg veya MultiOrg -- auth için kullanılmazsa geçerlidir.", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hans.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hans.json index 27bbe0d23ea5..b17c3da9b57a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hans.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hans.json @@ -4,6 +4,7 @@ "description": "用于创建支持服务器端呈现和客户端交互的 Blazor Web 应用的项目模板。此模板可用于具有丰富动态用户界面 (UI) 的 Web 应用。", "symbols/Framework/description": "项目的目标框架。", "symbols/Framework/choices/net8.0/description": "目标 net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "如果指定,则在创建时跳过项目的自动还原。", "symbols/ExcludeLaunchSettings/description": "是否从生成的模板中排除 launchSettings.json。", "symbols/kestrelHttpPort/description": "要用于 launchSettings.json 中 HTTP 终结点的端口号。", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "包含示例页(_I)", "symbols/IncludeSampleContent/description": "配置是否添加示例页和样式以演示基本使用模式。", "symbols/Empty/description": "配置是否忽略演示基本使用模式的示例页和样式。", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "是否禁用 HTTPS。仅当 Individual、IndividualB2C、SingleOrg 或 MultiOrg 不用于 --auth 时,此选项才适用。", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hant.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hant.json index f1c7a0d88ba7..5155a158b99b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hant.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/localize/templatestrings.zh-Hant.json @@ -4,6 +4,7 @@ "description": "用於建立同時支援伺服器端轉譯和用戶端互動的 Blazor Web 應用程式的專案範本。此範本可用於具有豐富動態使用者介面 (UI) 的 Web 應用程式。", "symbols/Framework/description": "專案的目標 Framework。", "symbols/Framework/choices/net8.0/description": "目標 net8.0", + "symbols/UserSecretsId/description": "The ID to use for secrets (use with Individual auth).", "symbols/skipRestore/description": "若指定,會在建立時跳過專案的自動還原。", "symbols/ExcludeLaunchSettings/description": "是否要從產生的範本排除 launchSettings.json。", "symbols/kestrelHttpPort/description": "launchSettings.json 中 HTTP 端點要使用的連接埠號碼。", @@ -29,6 +30,10 @@ "symbols/IncludeSampleContent/displayName": "包含範例頁面(_I)", "symbols/IncludeSampleContent/description": "設定是否要新增範例頁面和樣式,以示範基本使用模式。", "symbols/Empty/description": "設定是否要省略範例頁面和樣式,其示範基本使用模式。", + "symbols/auth/choices/None/description": "No authentication", + "symbols/auth/choices/Individual/description": "Individual authentication", + "symbols/auth/description": "The type of authentication to use", + "symbols/UseLocalDB/description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified.", "symbols/AllInteractive/displayName": "_Enable interactive rendering globally throughout the site", "symbols/AllInteractive/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.", "symbols/NoHttps/description": "是否要關閉 HTTPS。只有當 Individual、IndividualB2C、SingleOrg 或 MultiOrg 未用於 --auth 時,才適用此選項。", 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 971eb62028ba..6c5c05e1161b 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 @@ -14,7 +14,8 @@ "guids": [ "4C26868E-5E7C-458D-82E3-040509D0C71F", "5990939C-7E7B-4CFA-86FF-44CA5756498A", - "650B3CE7-2E93-4CC4-9F46-466686815EAA" + "650B3CE7-2E93-4CC4-9F46-466686815EAA", + "53bc9b9d-9d6a-45d4-8429-2a2761773502" // Client ID ], "identity": "Microsoft.Web.Blazor.CSharp.8.0", "thirdPartyNotices": "https://aka.ms/aspnetcore/8.0-third-party-notices", @@ -108,6 +109,41 @@ "BlazorWeb-CSharp.Client/Pages/**", "BlazorWeb-CSharp.Client/wwwroot/**" ] + }, + { + "condition": "(!IndividualLocalAuth)", + "exclude": [ + "BlazorWeb-CSharp/Components/Pages/Account/**", + "BlazorWeb-CSharp/Data/**", + "BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs", + "BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs", + "BlazorWeb-CSharp.Client/UserInfo.cs", + "BlazorWeb-CSharp.Client/Pages/Auth.razor" + ] + }, + { + "condition": "(!IndividualLocalAuth || UseLocalDB)", + "exclude": [ + "BlazorWeb-CSharp/app.db" + ] + }, + { + "condition": "(IndividualLocalAuth && UseLocalDB)", + "rename": { + "BlazorWeb-CSharp/Data/SqlServer/": "BlazorWeb-CSharp/Data/Migrations/" + }, + "exclude": [ + "BlazorWeb-CSharp/Data/SqlLite/**" + ] + }, + { + "condition": "(IndividualLocalAuth && !UseLocalDB)", + "rename": { + "BlazorWeb-CSharp/Data/SqlLite/": "BlazorWeb-CSharp/Data/Migrations/" + }, + "exclude": [ + "BlazorWeb-CSharp/Data/SqlServer/**" + ] } ] } @@ -130,6 +166,13 @@ "type": "bind", "binding": "HostIdentifier" }, + "UserSecretsId": { + "type": "parameter", + "datatype": "string", + "replaces": "aspnet-BlazorWeb-CSharp-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "defaultValue": "aspnet-BlazorWeb-CSharp-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "description": "The ID to use for secrets (use with Individual auth)." + }, "skipRestore": { "type": "parameter", "datatype": "bool", @@ -167,7 +210,7 @@ "kestrelHttpsPort": { "type": "parameter", "datatype": "integer", - "description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used)." + "description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if Individual auth is used)." }, "kestrelHttpsPortGenerated": { "type": "generated", @@ -207,7 +250,7 @@ "iisHttpsPort": { "type": "parameter", "datatype": "integer", - "description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used)." + "description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if Individual auth is used)." }, "iisHttpsPortGenerated": { "type": "generated", @@ -296,6 +339,28 @@ "defaultValue": "false", "description": "Configures whether to omit sample pages and styling that demonstrate basic usage patterns." }, + "auth": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "None", + "description": "No authentication" + }, + { + "choice": "Individual", + "description": "Individual authentication" + } + ], + "defaultValue": "None", + "description": "The type of authentication to use" + }, + "UseLocalDB": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual is specified." + }, "SampleContent": { "type": "computed", "value": "(((IncludeSampleContent && (HostIdentifier != \"dotnetcli\" && HostIdentifier != \"dotnetcli-preview\"))) || ((!Empty && (HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\"))))" @@ -312,9 +377,13 @@ "type": "computed", "value": "(InteractivityLocation == \"InteractiveGlobal\" || AllInteractive)" }, + "IndividualLocalAuth": { + "type": "computed", + "value": "(auth == \"Individual\")" + }, "RequiresHttps": { "type": "computed", - "value": "(OrganizationalAuth || IndividualAuth)" + "value": "(OrganizationalAuth || IndividualLocalAuth)" }, "HasHttpProfile": { "type": "computed", @@ -328,7 +397,7 @@ "type": "parameter", "datatype": "bool", "defaultValue": "false", - "description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth." + "description": "Whether to turn off HTTPS. This option only applies if Individual isn't used for --auth." }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor new file mode 100644 index 000000000000..e15f54ff654d --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor @@ -0,0 +1,14 @@ +@page "/auth" + +@using Microsoft.AspNetCore.Authorization; + +@attribute [Authorize] +@attribute [RenderModeAuto] + +Auth + +

You are authenticated

+ + +

Hello @context.User.Identity?.Name!

+
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs new file mode 100644 index 000000000000..f4e554f09a8b --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; + +namespace BlazorWeb_CSharp.Client; + +public class PersistedAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider +{ + private static readonly Task _unauthenticatedTask = + Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + + public override Task GetAuthenticationStateAsync() + { + // REVIEW: Is TryTakeFromJson correctly annotated? "|| userInfo is null" should not be necessary. + if (!persistentState.TryTakeFromJson(nameof(UserInfo), out var userInfo) || userInfo is null) + { + return _unauthenticatedTask; + } + + Claim[] claims = [ + new Claim(ClaimTypes.NameIdentifier, userInfo.UserId), + new Claim(ClaimTypes.Name, userInfo.Email), + new Claim(ClaimTypes.Email, userInfo.Email) ]; + + return Task.FromResult( + new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, + authenticationType: nameof(PersistedAuthenticationStateProvider))))); + } +} + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs index 519269f21bb8..560bc77dbced 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs @@ -1,5 +1,15 @@ +#if (IndividualLocalAuth) +using BlazorWeb_CSharp.Client; +using Microsoft.AspNetCore.Components.Authorization; +#endif using Microsoft.AspNetCore.Components.WebAssembly.Hosting; var builder = WebAssemblyHostBuilder.CreateDefault(args); +#if (IndividualLocalAuth) +builder.Services.AddAuthorizationCore(); +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddSingleton(); + +#endif await builder.Build().RunAsync(); 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 new file mode 100644 index 000000000000..236bbaa720da --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/UserInfo.cs @@ -0,0 +1,7 @@ +namespace BlazorWeb_CSharp.Client; + +public class UserInfo +{ + public required string UserId { get; set; } + public required string Email { get; set; } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/_Imports.razor index 5268e26fd6aa..cd618a113368 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/_Imports.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/_Imports.razor @@ -1,5 +1,8 @@ @using System.Net.Http @using System.Net.Http.Json +@*#if (IndividualLocalAuth) +@using Microsoft.AspNetCore.Components.Authorization +##endif*@ @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web 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/Layout/ManageLayout.razor new file mode 100644 index 000000000000..e4a7871bbc75 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ManageLayout.razor @@ -0,0 +1,17 @@ +@inherits LayoutComponentBase +@layout MainLayout + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ +
+
+ @Body +
+
+
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 new file mode 100644 index 000000000000..37ab9a430ca2 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/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/Layout/NavMenu.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor index 5b141a54826b..f9535bd7cb57 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 @@ -27,5 +27,37 @@ Weather + @*#if (IndividualLocalAuth && UseWebAssembly) + + + ##endif*@ + @*#if (IndividualLocalAuth) + + + + + + + + + + + ##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 95fcc36e0baa..0f1f79a45b85 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 @@ -46,6 +46,24 @@ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); } +/*#if (IndividualLocalAuth)*/ +.bi-lock { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); +} + +.bi-person { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); +} + +.bi-person-badge { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); +} + +.bi-person-fill { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); +} + +/*#endif*/ .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; 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 new file mode 100644 index 000000000000..378b2ec21fde --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmail.razor @@ -0,0 +1,48 @@ +@page "/Account/ConfirmEmail" + +@using System.Text +@using BlazorWeb_CSharp.Data +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject NavigationManager NavigationManager + +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) + { + NavigationManager.NavigateTo("/"); + } + 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/ForgotPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPassword.razor new file mode 100644 index 000000000000..19c98d628d5d --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPassword.razor @@ -0,0 +1,72 @@ +@page "/Account/ForgotPassword" + +@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; + +@inject NavigationManager NavigationManager +@inject UserManager UserManager +@inject IEmailSender EmailSender + +Forgot your password? + +

Forgot your password?

+

Enter your email.

+
+
+
+ + + + +
+ + + +
+ +
+
+
+ +@code { + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email); + if (user is null || !(await UserManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + NavigationManager.NavigateTo("/Account/ForgotPasswordConfirmation"); + return; + } + + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await UserManager.GeneratePasswordResetTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + $"{NavigationManager.BaseUri}Identity/Account/ResetPassword", + new Dictionary { { "code", code } }); + + await EmailSender.SendEmailAsync( + Input.Email, + "Reset Password", + $"Please reset your password by clicking here."); + + NavigationManager.NavigateTo("/Account/ForgotPasswordConfirmation"); + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = default!; + } +} 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/Pages/Account/ForgotPasswordConfirmation.razor new file mode 100644 index 000000000000..38de01d1ec0c --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPasswordConfirmation.razor @@ -0,0 +1,8 @@ +@page "/Account/ForgotPasswordConfirmation" + +Forgot password confirmation + +

Forgot password confirmation

+

+ Please check your email to reset your password. +

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/Pages/Account/InvalidPasswordReset.razor new file mode 100644 index 000000000000..509578bbf82c --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidPasswordReset.razor @@ -0,0 +1,8 @@ +@page "/Account/InvalidPasswordReset" + +Invalid password reset + +

Invalid password reset

+

+ The password reset link is invalid. +

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/Pages/Account/Login.razor new file mode 100644 index 000000000000..6d9fac8043b1 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Login.razor @@ -0,0 +1,147 @@ +@page "/Account/Login" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using BlazorWeb_CSharp.Data +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject SignInManager SignInManager +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IHttpContextAccessor HttpContextAccessor + +Log in + +

Log in

+
+
+
+ + +

Use a local account to log in.

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

Use another service to log in.

+
+ @if ((externalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + about setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
{ ["ReturnUrl"] = ReturnUrl })))" method="post" class="form-horizontal"> +
+

+ @foreach (var provider in externalLogins!) + { + + } +

+
+
+ } +
+
+
+ +@code { + string? errorMessage; + IList? externalLogins; + + [SupplyParameterFromForm] + public 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; + } + + protected override async Task OnInitializedAsync() + { + // Clear the existing external cookie to ensure a clean login process + // No access to HttpContext: https://github.com/dotnet/aspnetcore/issues/48769 + await HttpContextAccessor.HttpContext!.SignOutAsync(IdentityConstants.ExternalScheme); + + externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + } + + public async Task LoginUser() + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + Logger.LogInformation("User logged in."); + NavigationManager.NavigateTo(ReturnUrl); + } + if (result.RequiresTwoFactor) + { + NavigationManager.NavigateTo(NavigationManager.GetUriWithQueryParameters("/Account/LoginWith2fa", + new Dictionary { ["ReturnUrl"] = ReturnUrl, ["RememberMe"] = Input.RememberMe })); + } + if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + NavigationManager.NavigateTo("/Account/Lockout"); + } + else + { + errorMessage = "Error: Invalid login attempt."; + } + } +} 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/Pages/Account/Manage/ChangePassword.razor new file mode 100644 index 000000000000..374ed75ae741 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ChangePassword.razor @@ -0,0 +1,107 @@ +@page "/Account/Manage/ChangePassword" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject NavigationManager NavigationManager +@inject ILogger Logger + +Change password + +

Change password

+ +
+
+ + + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + private string? _message; + private ApplicationUser? _user; + private bool _hasPassword; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + [CascadingParameter] + private Task? AuthenticationStateTask { get; set; } + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + (_user, _message) = await UserManager.GetUserAsync(AuthenticationStateTask); + if (_user is null) + { + return; + } + + _hasPassword = await UserManager.HasPasswordAsync(_user); + if (!_hasPassword) + { + NavigationManager.NavigateTo("/Account/Manage/SetPassword"); + return; + } + } + + private async Task OnValidSubmitAsync() + { + if (_user is null) + { + return; + } + + var changePasswordResult = await UserManager.ChangePasswordAsync(_user, Input.OldPassword!, Input.NewPassword!); + if (!changePasswordResult.Succeeded) + { + _message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}"; + return; + } + + await SignInManager.RefreshSignInAsync(_user); + Logger.LogInformation("User changed their password successfully."); + _message = "Your password has been changed."; + } + + private sealed class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string? OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string? NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string? ConfirmPassword { get; set; } + } +} diff --git a/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 new file mode 100644 index 000000000000..ab2a4b4cc536 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Email.razor @@ -0,0 +1,138 @@ +@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 + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager + +Manage email + +

Manage email

+ + +
+
+
+ + + + + + @if (_isEmailConfirmed) + { +
+ +
+ +
+ +
+ } + else + { +
+ + + +
+ } +
+ + + +
+ +
+
+
+ +@code { + private string? _message; + private ApplicationUser? _user; + private string? _email; + private bool _isEmailConfirmed; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + [CascadingParameter] + private Task? AuthenticationStateTask { get; set; } + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + (_user, _message) = await UserManager.GetUserAsync(AuthenticationStateTask); + if (_user is null) + { + return; + } + + _email = await UserManager.GetEmailAsync(_user); + _isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(_user); + + Input.NewEmail ??= _email; + } + + private async Task OnValidSubmitAsync() + { + if (_user is null) + { + return; + } + + 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}Identity/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."); + _message = "Confirmation link to change email sent. Please check your email."; + return; + } + + _message = "Your email is unchanged."; + } + + private async Task OnSendEmailVerificationAsync() + { + if (_user 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.BaseUri}Identity/Account/ConfirmEmail", + new Dictionary { { "userId", userId }, { "code", code } }); + await EmailSender.SendEmailAsync( + _email!, + "Confirm your email", + $"Please confirm your account by clicking here."); + + _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/ExternalLogins.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ExternalLogins.razor new file mode 100644 index 000000000000..75c7538ad9d4 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ExternalLogins.razor @@ -0,0 +1,7 @@ +@page "/Account/Manage/ExternalLogins" + +Manage your external logins + +

Manage your external logins

+ +@* TODO: Implement this *@ 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/Pages/Account/Manage/Index.razor new file mode 100644 index 000000000000..d0a42c7d2883 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Index.razor @@ -0,0 +1,92 @@ +@page "/Account/Manage" + +@using System.ComponentModel.DataAnnotations; +@using System.Security.Claims +@using Microsoft.AspNetCore.Identity; +@using BlazorWeb_CSharp.Data; + +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject NavigationManager NavigationManager + +Profile + +

Profile

+ + +
+
+ + + +
+ + +
+
+ + + +
+ +
+
+
+ +@code { + private string? _message; + private ApplicationUser? _user; + private string? _username; + private string? _phoneNumber; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + [CascadingParameter] + private Task? AuthenticationStateTask { get; set; } + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + (_user, _message) = await UserManager.GetUserAsync(AuthenticationStateTask); + if (_user is null) + { + return; + } + + _username = await UserManager.GetUserNameAsync(_user); + _phoneNumber = await UserManager.GetPhoneNumberAsync(_user); + + Input.PhoneNumber ??= _phoneNumber; + } + + private async Task OnValidSubmitAsync() + { + if (_user is null) + { + return; + } + + if (Input.PhoneNumber != _phoneNumber) + { + var setPhoneResult = await UserManager.SetPhoneNumberAsync(_user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + _message = "Unexpected error when trying to set phone number."; + return; + } + } + + await SignInManager.RefreshSignInAsync(_user); + _message = "Your profile has been updated"; + } + + private sealed class InputModel + { + [Phone] + [Display(Name = "Phone number")] + public string? PhoneNumber { get; set; } + } +} 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/Pages/Account/Manage/PersonalData.razor new file mode 100644 index 000000000000..01421ec9fda5 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/PersonalData.razor @@ -0,0 +1,7 @@ +@page "/Account/Manage/PersonalData" + +Personal Data + +

Personal Data

+ +@* TODO: Implement this *@ 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 new file mode 100644 index 000000000000..dc49aa4edecd --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/TwoFactorAuthentication.razor @@ -0,0 +1,7 @@ +@page "/Account/Manage/TwoFactorAuthentication" + +Two-factor authentication (2FA) + +

Two-factor authentication (2FA)

+ +@* TODO: Implement this *@ 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 new file mode 100644 index 000000000000..d605aec1f504 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/_Imports.razor @@ -0,0 +1,2 @@ +@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 new file mode 100644 index 000000000000..99b5304fc019 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Register.razor @@ -0,0 +1,203 @@ +@page "/Account/Register" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using BlazorWeb_CSharp.Data +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Identity.UI.Services +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject IUserStore UserStore +@inject SignInManager SignInManager +@inject ILogger Logger +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager + +Register + +

Register

+ +
+
+ + + +

Create a new account.

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

Use another service to register.

+
+ @{ + if ((ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + about setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in ExternalLogins!) + { + + } +

+
+
+ } + } +
+
+
+ +@code { + [SupplyParameterFromForm] + public InputModel Input { get; set; } = new(); + + /// + /// 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 IList? ExternalLogins { 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 async Task OnInitializedAsync() + { + ExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + } + + 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) + { + NavigationManager.NavigateTo(NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("/Account/RegisterConfirmation").AbsoluteUri, + new Dictionary { { "email", Input.Email }, { "returnUrl", ReturnUrl } })); + } + else + { + await SignInManager.SignInAsync(user, isPersistent: false); + NavigationManager.NavigateTo(NavigationManager.ToAbsoluteUri(NavigationManager.ToBaseRelativePath(ReturnUrl)).AbsoluteUri); + } + } + 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; + } +} \ No newline at end of file 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 new file mode 100644 index 000000000000..e03daa4aaa18 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/RegisterConfirmation.razor @@ -0,0 +1,65 @@ +@page "/Account/RegisterConfirmation" + +@using System.Text +@using BlazorWeb_CSharp.Data +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Identity.UI.Services +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager + +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) + { + NavigationManager.NavigateTo("/"); + } + 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/ResetPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPassword.razor new file mode 100644 index 000000000000..21a057e1bf4e --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPassword.razor @@ -0,0 +1,107 @@ +@page "/Account/ResetPassword" + +@using System.ComponentModel.DataAnnotations; +@using System.Text; +@using Microsoft.AspNetCore.Http; +@using Microsoft.AspNetCore.Identity; +@using Microsoft.AspNetCore.WebUtilities; +@using BlazorWeb_CSharp.Data; + +@inject NavigationManager NavigationManager +@inject UserManager UserManager + +Reset password + +

Reset password

+

Reset your password.

+
+
+
+ + + + + + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + [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)); + + protected override void OnInitialized() + { + if (Code is null) + { + NavigationManager.NavigateTo("/Account/InvalidPasswordReset"); + } + else + { + Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + } + } + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email); + if (user is null) + { + // Don't reveal that the user does not exist + NavigationManager.NavigateTo("/Account/ResetPasswordConfirmation"); + return; + } + + var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + NavigationManager.NavigateTo("/Account/ResetPasswordConfirmation"); + return; + } + + identityErrors = result.Errors; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = default!; + + [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!; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = default!; + + [Required] + public string Code { get; set; } = default!; + } +} 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/Pages/Account/ResetPasswordConfirmation.razor new file mode 100644 index 000000000000..273c8247bd14 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResetPasswordConfirmation.razor @@ -0,0 +1,7 @@ +@page "/Account/ResetPasswordConfirmation" +Reset password confirmation + +

Reset password confirmation

+

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

diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor new file mode 100644 index 000000000000..9dfc84b03c52 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor @@ -0,0 +1,12 @@ +@if (!String.IsNullOrEmpty(Message)) +{ + var statusMessageClass = Message.StartsWith("Error") ? "danger" : "success"; + +} + +@code { + [Parameter] public string? Message { get; set; } +} \ No newline at end of file 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 25a5e5887bfe..1426521804d1 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 @@ -4,7 +4,11 @@ ##endif*@ + @*#if (IndividualLocalAuth) + + ##else + ##endif*@ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/_Imports.razor index 68ab14b7c1e6..bb52859cae6d 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/_Imports.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/_Imports.razor @@ -1,5 +1,8 @@ @using System.Net.Http @using System.Net.Http.Json +@*#if (IndividualLocalAuth) +@using Microsoft.AspNetCore.Components.Authorization +##endif*@ @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationDbContext.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationDbContext.cs new file mode 100644 index 000000000000..08e9d0808401 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationDbContext.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace BlazorWeb_CSharp.Data; + +public class ApplicationDbContext : IdentityDbContext +{ + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationUser.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationUser.cs new file mode 100644 index 000000000000..831bb92a4a57 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/ApplicationUser.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace BlazorWeb_CSharp.Data; + +// Add profile data for application users by adding properties to the ApplicationUser class +public class ApplicationUser : IdentityUser +{ +} + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 000000000000..ba5b97c07dff --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,268 @@ +// +using System; +using BlazorWeb_CSharp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BlazorWeb_CSharp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("BlazorWeb_CSharp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 000000000000..bf83fc9a9f27 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,222 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BlazorWeb_CSharp.Migrations +{ + /// + public partial class CreateIdentitySchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 000000000000..930cebc1857d --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlLite/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,265 @@ +// +using System; +using BlazorWeb_CSharp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BlazorWeb_CSharp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("BlazorWeb_CSharp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 000000000000..34d2b6df1a30 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,279 @@ +// +using System; +using BlazorWeb_CSharp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BlazorWeb_CSharp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("BlazorWeb_CSharp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 000000000000..ec47e9f16b95 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,224 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BlazorWeb_CSharp.Migrations +{ + /// + public partial class CreateIdentitySchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 000000000000..a6b6896c65dd --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/SqlServer/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,276 @@ +// +using System; +using BlazorWeb_CSharp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BlazorWeb_CSharp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("BlazorWeb_CSharp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BlazorWeb_CSharp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs new file mode 100644 index 000000000000..6d3808584a60 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Components.Authorization; + +namespace Microsoft.AspNetCore.Identity; + +internal static class UserManagerExtensions +{ + public static async Task<(TUser? User, string? Error)> GetUserAsync(this UserManager userManager, Task? authenticationStateTask) + where TUser : class + { + if (authenticationStateTask is null) + { + return (User: null, Error: "Unable to authenticate user."); + } + + var authenticationState = await authenticationStateTask; + var user = await userManager.GetUserAsync(authenticationState.User); + if (user is null) + { + return (User: null, Error: $"Unable to load user with ID '{userManager.GetUserId(authenticationState.User)}'."); + } + + return (User: user, Error: null); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs new file mode 100644 index 000000000000..362b62609fc9 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs @@ -0,0 +1,51 @@ +using BlazorWeb_CSharp.Client; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace BlazorWeb_CSharp; + +public class PersistingAuthenticationStateProvider : AuthenticationStateProvider, IDisposable +{ + private readonly IHttpContextAccessor _contextAccessor; + private readonly PersistingComponentStateSubscription _subscription; + + public PersistingAuthenticationStateProvider(IHttpContextAccessor contextAccessor, PersistentComponentState state, IOptions identityOptions) + { + _contextAccessor = contextAccessor; + + _subscription = state.RegisterOnPersisting(() => + { + var user = RequiredHttpContext.User; + + if (user.Identity?.IsAuthenticated == true) + { + var userId = user.FindFirst(identityOptions.Value.ClaimsIdentity.UserIdClaimType)?.Value; + var email = user.FindFirst(identityOptions.Value.ClaimsIdentity.EmailClaimType)?.Value; + + if (userId != null && email != null) + { + state.PersistAsJson(nameof(UserInfo), new UserInfo + { + UserId = userId, + Email = email, + }); + } + } + + return Task.CompletedTask; + }); + } + + private HttpContext RequiredHttpContext => + _contextAccessor.HttpContext ?? throw new InvalidOperationException("IHttpContextAccessor HttpContext AsyncLocal missing!"); + + public override Task GetAuthenticationStateAsync() => + Task.FromResult(new AuthenticationState(RequiredHttpContext.User)); + + public void Dispose() + { + _subscription.Dispose(); + } +} 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 c241a8dd7610..7d805af202b7 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 @@ -1,7 +1,17 @@ +#if (IndividualLocalAuth) +using BlazorWeb_CSharp; +#endif #if (UseWebAssembly) using BlazorWeb_CSharp.Client.Pages; #endif using BlazorWeb_CSharp.Components; +#if (IndividualLocalAuth) +using BlazorWeb_CSharp.Data; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.EntityFrameworkCore; +#endif namespace BlazorWeb_CSharp; @@ -26,6 +36,30 @@ public static void Main(string[] args) #endif #endif + #if (IndividualLocalAuth) + builder.Services.AddCascadingAuthenticationState(); + builder.Services.AddScoped(); + + builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) + .AddIdentityCookies(); + + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + builder.Services.AddDbContext(options => + #if (UseLocalDB) + options.UseSqlServer(connectionString)); + #else + options.UseSqlite(connectionString)); + #endif + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + + builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); + + builder.Services.AddSingleton(); + + #endif var app = builder.Build(); // Configure the HTTP request pipeline. 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 5470d2ce484d..857bf510ad44 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 @@ -1,7 +1,17 @@ +#if (IndividualLocalAuth) +using BlazorWeb_CSharp; +#endif #if (UseWebAssembly) using BlazorWeb_CSharp.Client.Pages; #endif using BlazorWeb_CSharp.Components; +#if (IndividualLocalAuth) +using BlazorWeb_CSharp.Data; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.EntityFrameworkCore; +#endif var builder = WebApplication.CreateBuilder(args); @@ -13,13 +23,37 @@ #if (UseServer && UseWebAssembly) .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); - #elif(UseServer) + #elif (UseServer) .AddInteractiveServerComponents(); - #elif(UseWebAssembly) + #elif (UseWebAssembly) .AddInteractiveWebAssemblyComponents(); #endif #endif +#if (IndividualLocalAuth) +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddScoped(); + +builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) + .AddIdentityCookies(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +builder.Services.AddDbContext(options => +#if (UseLocalDB) + options.UseSqlServer(connectionString)); +#else + options.UseSqlite(connectionString)); +#endif +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); + +builder.Services.AddSingleton(); + +#endif var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/app.db b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/app.db new file mode 100644 index 0000000000000000000000000000000000000000..769de58a9edcf5a1759ed6f6ce8bd0b2608e727e GIT binary patch literal 102400 zcmeI5%WvC89>+yLM89O^(L`Hxw`J(XHi92;l6Jj|MUkp9l?aw8MUt~Nia=!~CLtn; zjz~3i(LK0M1GL9piv{){*kezN9QTmx_O!=dd)|YAUUp_knnRkjY$OI8;Vak)Igj7` z=JR{ZP#h$+J0-)`r3YrOquJ7p#JR-CNaABjN+c2^wVJXV$8cCe+%4o+~r7O z>A^4Kv~l`Z+U9cl*V!MYFQuMMen0uK_+tE}_|@o-|SBR6*n7xhkRQzI#z=UEw;kN=UoNdk>fjt)o`Rz zT^zx3cJJL;F|)WRd}G?${kHCmv|;JJy7^e|TH*V&J90huNlEc0=r<>lQ&ST8$h553 zlBK$`S(nr@`Q0d$7Tn5``OxTAdgkXwOYgdd9j7VV!3gwhBxa%xvCUzslk!5QdKD^ zRYlD!H6EQs?Lk;&sn4t`C55xiYhZjKail zapZ-NECp@i$&0Xwt4W+qPND(zhg0#2k5b9Zo%z9D;o#t$f)&i}El;tie||CK6D$2HK=?koy^B5>}f;%t7fhPcU` zW_glxm0L>nAkj;)2Jm|G2FPvH%SDxpx2~vlZ;kL$Y2*UJq||q|BUS;K)g5Dr2;_1{ zt<$z)WgWE6O| zIs};qygfdO$d-X)&4)}(vh=r}bkLYaN=Gx=UQXsRt=~DW1uM^+-3LamqetWePs564 zSx?Pg>$YY+iWsP-H~T%q-l^GIXFH;vj6fRnNVvo{sXQ{fy4vsD_cxZ@Ot{)(Xj^}3 z7PO{q_LMH&8y_*RlG%K0_HCtmu;b+hJ*XTYB-bQaz$y?M=&iij@7jm+Dsi_DlgUhR zZt%))@moPl+`Tu-ymU|Cyp#rs>!s{+EXqq+&56C#?G@prG0x{ZSxkc?j8CzQ4aUNB z^02U*@!d^9%&e>kUzZt{MnfsA8xMP$ZJ1r_wqX(D??e`k1W`q{@u3~aJe2&xF&-l* zf+N|ZwE9i^F4iaU_|!w-VfP3MHY`bPt-~$36 z00JNY0w4eaAOHd&00JNY0w8df3H(_Yt=(7-d>Z+luG!?9S-or1uiu)F^p5tYrRAmN z#a?si`qK3qAN=w9(zOq!$h?H~e-q>f9}oZm5C8!X009sH0T2KI5C8!X0D(7+z#m3y zlYwspmLtCspzr_5p?`cp00ck)1V8`;KmY_l00ck)1V8`;hL`|-|BvVYAvQ3k1_B@e z0w4eaAOHd&00JNY0w4eagaF?E!woT z8G--^fB*=900@8p2!H?xfB*=90RH_y_y7oi00@8p2!H?xfB*=900@8p2n;^~y#F75 z8)Jqb00JNY0w4eaAOHd&00JNY0w93*|L_42009sH0T2KI5C8!X009sH0T38|0(k#F z{5Hl6K>!3m00ck)1V8`;KmY_l00ck)@BiTgAOHd&00JNY0w4eaAOHd&00JN|`~>j) zKm0bv3_$<{KmY_l00ck)1V8`;KmY_l0MGyM0T2KI5C8!X009sH0T2KI5C8!X7=8kH z{vUoDV}>9A0w4eaAOHd&00JNY0w4eaAdpJ$Cnm)giP`_8|C##N><`nIQqLy8pZr*S zG5%8gYV^nP?}VR5ZK0g_IdSjAw9@MnqAFT&0a@q8(-=zDpk(z3g^VA8Pg8#jOnE}W54)( z%#8gO4X0yR4M!@~#jVe#_s4(9EG`P)n6`Gmt^085=3~8Uh40hu$o1SOC530{G$)c% zQxf@jM5Vg2S(nr@`Q0d$7Tn5``OxTAdgkXwOYh0iZ5X2Hs=HdduY28mvH7xEt5B$;P#?#m=So$0joK^TP8_Ll&kkz!{j`rsC|&YL97N*S8{#$I}cL$sHihMy|~4 zAfqrbE*9t7?HRI(CojS#tXsTIPND(LE2rWWAElC+JM)9R!a=|}1uK|M?+gEIeG8b8T>&h*qdXVU)SOa*yc?0A&>gA$J##>j^y0=ESOLDXiF)8((?TA%CW_8C{ zA_BSGQR}pASeZ!DOuWSm;O25mf6~{xO+9ow8NeT^yxJxEhXv9zyO$6T8Q_+#Tg4b$PU#(MJ49EigtUWGf=H`TN-wVYRmqx8* z6nM5e1epiG#77a?GH|T~bv1OIgi{z0~a$;iWOo=Q~+UgCmSjv5XDI z!gTVmu$%GSO+n18tO#G18J0#vDXbe0dzx*SUF)`C5##Sf7LEi_MYi#w9mqVC{K7FF zBPfC+*`u`jP5Um{VG?D%XmPhvgh+8DnYp(T=O~fmG_FM!kCJEk3Hm&b_x~sURs$VC z00ck)1V8`;KmY_l00ck)1VG@8BS8QEzf|&NVs?FYW@cr2f9kJOm&yD8-%tEw;_p{l8wmTmxYJ>jXRusj?Hf~U=Zh}T=s8oiJ6=8!tU?b2!T^@&WjKF zr+1c&D#l7P!9ioYg>_(^>0}+L^f24S$ir-OB=<}Lz2p^eiZ30ZL7-z;1*ekg;0UHV zT!_`deZwEt!70WOLhrOCHK&WpwJdjN(y!z#|H>!S_THU!4l&>iqI)FrjGaMz#nWbc z)fo{d5W}ZJy~O#3o~3kH83=#?2!H?x zfB*=900@8p2!H?xye Date: Mon, 18 Sep 2023 12:19:50 -0700 Subject: [PATCH 02/18] Include updates from https://github.com/halter73/BlazorWebIdentity/pull/2 --- .../.template.config/template.json | 2 + .../Identity/ExternalLoginPicker.razor | 46 ++++ .../Components/Identity/StatusMessage.razor | 20 ++ .../Pages/Account/ConfirmEmail.razor | 2 +- .../Pages/Account/ConfirmEmailChange.razor | 64 ++++++ .../Pages/Account/ExternalLogin.razor | 215 ++++++++++++++++++ .../Pages/Account/ForgotPassword.razor | 6 +- .../Pages/Account/InvalidUser.razor | 7 + .../Components/Pages/Account/Lockout.razor | 8 + .../Components/Pages/Account/Login.razor | 50 ++-- .../Pages/Account/LoginWith2fa.razor | 108 +++++++++ .../Pages/Account/LoginWithRecoveryCode.razor | 93 ++++++++ .../Pages/Account/Manage/ChangePassword.razor | 23 +- .../Account/Manage/DeletePersonalData.razor | 84 +++++++ .../Pages/Account/Manage/Disable2fa.razor | 67 ++++++ .../Pages/Account/Manage/Email.razor | 37 +-- .../Account/Manage/EnableAuthenticator.razor | 168 ++++++++++++++ .../Pages/Account/Manage/ExternalLogins.razor | 148 +++++++++++- .../Manage/GenerateRecoveryCodes.razor | 60 +++++ .../Pages/Account/Manage/Index.razor | 25 +- .../Pages/Account/Manage/PersonalData.razor | 30 ++- .../Account/Manage/ResetAuthenticator.razor | 54 +++++ .../Pages/Account/Manage/SetPassword.razor | 87 +++++++ .../Account/Manage/ShowRecoveryCodes.razor | 43 ++++ .../Manage/TwoFactorAuthentication.razor | 97 +++++++- .../Components/Pages/Account/Register.razor | 45 +--- .../Pages/Account/RegisterConfirmation.razor | 2 +- .../Account/ResendEmailConfirmation.razor | 76 +++++++ .../Pages/Account/ResetPassword.razor | 6 +- .../Pages/Account/StatusMessage.razor | 12 - .../BlazorWeb-CSharp/Data/UserAccessor.cs | 28 +++ .../Data/UserManagerExtensions.cs | 24 -- ...omponentsEndpointRouteBuilderExtensions.cs | 118 ++++++++++ .../RedirectNavigationManagerExtensions.cs | 38 ++++ .../BlazorWeb-CSharp/Program.Main.cs | 6 + .../BlazorWeb-CSharp/Program.cs | 6 + 36 files changed, 1724 insertions(+), 181 deletions(-) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ExternalLoginPicker.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/StatusMessage.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmailChange.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ExternalLogin.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidUser.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Lockout.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWith2fa.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWithRecoveryCode.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/DeletePersonalData.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Disable2fa.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/EnableAuthenticator.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/GenerateRecoveryCodes.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ResetAuthenticator.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/SetPassword.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/ShowRecoveryCodes.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResendEmailConfirmation.razor delete mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserAccessor.cs delete mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/RedirectNavigationManagerExtensions.cs 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 6c5c05e1161b..a49e8d59f673 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 @@ -114,7 +114,9 @@ "condition": "(!IndividualLocalAuth)", "exclude": [ "BlazorWeb-CSharp/Components/Pages/Account/**", + "BlazorWeb-CSharp/Components/Identity/**", "BlazorWeb-CSharp/Data/**", + "BlazorWeb-CSharp/Identity/**", "BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs", "BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs", "BlazorWeb-CSharp.Client/UserInfo.cs", 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/Identity/ExternalLoginPicker.razor new file mode 100644 index 000000000000..d67edc9db99a --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/ExternalLoginPicker.razor @@ -0,0 +1,46 @@ +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Components.Pages.Account +@using BlazorWeb_CSharp.Data + +@inject SignInManager SignInManager +@inject NavigationManager NavigationManager + +@if ((_externalLogins?.Count ?? 0) == 0) +{ +
+

+ There are no external authentication services configured. See this article + about setting up this ASP.NET application to support logging in via external services. +

+
+} +else +{ +
+
+ + +

+ @foreach (var provider in _externalLogins!) + { + + } +

+
+
+} + +@code { + private IList? _externalLogins; + + [SupplyParameterFromQuery] + private string ReturnUrl { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + ReturnUrl ??= "/"; + + _externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + } +} 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 new file mode 100644 index 000000000000..33953c94bc52 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/StatusMessage.razor @@ -0,0 +1,20 @@ +@{ + var message = Message ?? MessageFromQuery; +} + +@if (!string.IsNullOrEmpty(message)) +{ + var statusMessageClass = message.StartsWith("Error") ? "danger" : "success"; + +} + +@code { + [Parameter] + public string? Message { get; set; } + + [SupplyParameterFromQuery(Name = "Message")] + public string? MessageFromQuery { get; set; } +} 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 index 378b2ec21fde..66a4f5b34ca7 100644 --- 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 @@ -26,7 +26,7 @@ { if (UserId == null || Code == null) { - NavigationManager.NavigateTo("/"); + NavigationManager.RedirectTo("/"); } else { 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/Pages/Account/ConfirmEmailChange.razor new file mode 100644 index 000000000000..08987693b08f --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmailChange.razor @@ -0,0 +1,64 @@ +@page "/Account/ConfirmEmailChange" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using BlazorWeb_CSharp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject UserAccessor UserAccessor +@inject NavigationManager NavigationManager + +Confirm email change + +

Confirm email change

+ + + +@code { + private string? _message; + private ApplicationUser _user = default!; + + [SupplyParameterFromQuery] + private string? UserId { get; set; } + + [SupplyParameterFromQuery] + private string? Email { get; set; } + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + protected override async Task OnInitializedAsync() + { + if (UserId is null || Email is null || Code is null) + { + NavigationManager.RedirectTo( + "/Account/Login", + new() { ["Message"] = "Error: Invalid email change confirmation link." }); + return; + } + + _user = await UserAccessor.GetRequiredUserAsync(); + + var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + var result = await UserManager.ChangeEmailAsync(_user, Email, code); + if (!result.Succeeded) + { + _message = "Error changing email."; + return; + } + + // In our UI email and user name are one and the same, so when we update the email + // we need to update the user name. + var setUserNameResult = await UserManager.SetUserNameAsync(_user, Email); + if (!setUserNameResult.Succeeded) + { + _message = "Error changing user name."; + return; + } + + await SignInManager.RefreshSignInAsync(_user); + _message = "Thank you for confirming your email change."; + } +} diff --git a/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/Pages/Account/ExternalLogin.razor new file mode 100644 index 000000000000..4d9229a9b6ce --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ExternalLogin.razor @@ -0,0 +1,215 @@ +@page "/Account/ExternalLogin" + +@using System.ComponentModel.DataAnnotations +@using System.Security.Claims +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Identity.UI.Services +@using Microsoft.AspNetCore.WebUtilities +@using BlazorWeb_CSharp.Data + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject IUserStore UserStore +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject ILogger Logger + +@{ + var providerDisplayName = _externalLoginInfo.ProviderDisplayName; +} + +Register + + +

Register

+

Associate your @providerDisplayName account.

+
+ +

+ You've successfully authenticated with @providerDisplayName. + Please enter an email address for this site below and click the Register button to finish + logging in. +

+ +
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + public const string LoginCallbackAction = "LoginCallback"; + + private string? _message; + private ExternalLoginInfo _externalLoginInfo = default!; + private IUserEmailStore _emailStore = default!; + + [SupplyParameterFromQuery] + private string? RemoteError { get; set; } + + [CascadingParameter] + public HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + [SupplyParameterFromQuery] + private string ReturnUrl { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? Action { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + ReturnUrl ??= "/"; + + if (RemoteError is not null) + { + NavigationManager.RedirectTo( + "/Account/Login", + new() { ["Message"] = "Error from external provider: " + RemoteError }); + return; + } + + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo is null) + { + NavigationManager.RedirectTo( + "/Account/Login", + new() { ["Message"] = "Error loading external login information." }); + return; + } + + _externalLoginInfo = externalLoginInfo; + _emailStore = GetEmailStore(); + + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + if (Action == LoginCallbackAction) + { + await OnLoginCallbackAsync(); + return; + } + + // We should only reach this page via the login callback, so redirect back to + // the login page if we get here some other way. + NavigationManager.RedirectTo("/Account/Login"); + return; + } + } + + private async Task OnLoginCallbackAsync() + { + // Sign in the user with this external login provider if the user already has a login. + var result = await SignInManager.ExternalLoginSignInAsync( + _externalLoginInfo.LoginProvider, + _externalLoginInfo.ProviderKey, + isPersistent: false, + bypassTwoFactor: true); + if (result.Succeeded) + { + Logger.LogInformation( + "{Name} logged in with {LoginProvider} provider.", + _externalLoginInfo.Principal.Identity?.Name, + _externalLoginInfo.LoginProvider); + NavigationManager.RedirectTo(ReturnUrl); + return; + } + + if (result.IsLockedOut) + { + NavigationManager.RedirectTo("/Account/Lockout"); + return; + } + + // If the user does not have an account, then ask the user to create an account. + if (_externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) + { + Input.Email = _externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email); + } + } + + private async Task OnValidSubmitAsync() + { + var user = CreateUser(); + + await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); + await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); + + var result = await UserManager.CreateAsync(user); + if (result.Succeeded) + { + result = await UserManager.AddLoginAsync(user, _externalLoginInfo); + if (result.Succeeded) + { + Logger.LogInformation("User created an account using {Name} provider.", _externalLoginInfo.LoginProvider); + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + $"{NavigationManager.BaseUri}Account/ConfirmEmail", + new Dictionary { { "userId", userId }, { "code", code } }); + await EmailSender.SendEmailAsync(Input.Email!, "Confirm your email", + $"Please confirm your account by clicking here."); + + // 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) + { + NavigationManager.RedirectTo("/Account/RegisterConfirmation", new() { ["Email"] = Input.Email }); + return; + } + + await SignInManager.SignInAsync(user, isPersistent: false, _externalLoginInfo.LoginProvider); + NavigationManager.RedirectTo(ReturnUrl); + return; + } + } + else + { + _message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}"; + } + } + + 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] + 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/Pages/Account/ForgotPassword.razor index 19c98d628d5d..a7dcdf9f60df 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/Pages/Account/ForgotPassword.razor @@ -43,7 +43,7 @@ if (user is null || !(await UserManager.IsEmailConfirmedAsync(user))) { // Don't reveal that the user does not exist or is not confirmed - NavigationManager.NavigateTo("/Account/ForgotPasswordConfirmation"); + NavigationManager.RedirectTo("/Account/ForgotPasswordConfirmation"); return; } @@ -52,7 +52,7 @@ var code = await UserManager.GeneratePasswordResetTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = NavigationManager.GetUriWithQueryParameters( - $"{NavigationManager.BaseUri}Identity/Account/ResetPassword", + $"{NavigationManager.BaseUri}Account/ResetPassword", new Dictionary { { "code", code } }); await EmailSender.SendEmailAsync( @@ -60,7 +60,7 @@ "Reset Password", $"Please reset your password by clicking here."); - NavigationManager.NavigateTo("/Account/ForgotPasswordConfirmation"); + NavigationManager.RedirectTo("/Account/ForgotPasswordConfirmation"); } private sealed class InputModel 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/Pages/Account/InvalidUser.razor new file mode 100644 index 000000000000..e61fe5def569 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/InvalidUser.razor @@ -0,0 +1,7 @@ +@page "/Account/InvalidUser" + +Invalid user + +

Invalid user

+ + 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/Pages/Account/Lockout.razor new file mode 100644 index 000000000000..a8d1e0afc7ca --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Lockout.razor @@ -0,0 +1,8 @@ +@page "/Account/Lockout" + +Locked out + +
+

Locked out

+

This account has been locked out, please try again later.

+
diff --git a/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/Pages/Account/Login.razor index 6d9fac8043b1..506e9243a157 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/Pages/Account/Login.razor @@ -10,7 +10,6 @@ @inject SignInManager SignInManager @inject ILogger Logger @inject NavigationManager NavigationManager -@inject IHttpContextAccessor HttpContextAccessor Log in @@ -60,38 +59,19 @@

Use another service to log in.


- @if ((externalLogins?.Count ?? 0) == 0) - { -
-

- There are no external authentication services configured. See this article - about setting up this ASP.NET application to support logging in via external services. -

-
- } - else - { -
{ ["ReturnUrl"] = ReturnUrl })))" method="post" class="form-horizontal"> -
-

- @foreach (var provider in externalLogins!) - { - - } -

-
-
- } +
@code { string? errorMessage; - IList? externalLogins; + + [CascadingParameter] + public HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] - public InputModel Input { get; set; } = new(); + public InputModel Input { get; set; } = default!; [SupplyParameterFromQuery] public string ReturnUrl { get; set; } = ""; @@ -112,11 +92,14 @@ protected override async Task OnInitializedAsync() { - // Clear the existing external cookie to ensure a clean login process - // No access to HttpContext: https://github.com/dotnet/aspnetcore/issues/48769 - await HttpContextAccessor.HttpContext!.SignOutAsync(IdentityConstants.ExternalScheme); + Input ??= new(); + ReturnUrl ??= "/"; - externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + } } public async Task LoginUser() @@ -127,17 +110,18 @@ if (result.Succeeded) { Logger.LogInformation("User logged in."); - NavigationManager.NavigateTo(ReturnUrl); + NavigationManager.RedirectTo(ReturnUrl); } if (result.RequiresTwoFactor) { - NavigationManager.NavigateTo(NavigationManager.GetUriWithQueryParameters("/Account/LoginWith2fa", - new Dictionary { ["ReturnUrl"] = ReturnUrl, ["RememberMe"] = Input.RememberMe })); + NavigationManager.RedirectTo( + "/Account/LoginWith2fa", + new() { ["ReturnUrl"] = ReturnUrl, ["RememberMe"] = Input.RememberMe }); } if (result.IsLockedOut) { Logger.LogWarning("User account locked out."); - NavigationManager.NavigateTo("/Account/Lockout"); + NavigationManager.RedirectTo("/Account/Lockout"); } else { 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/Pages/Account/LoginWith2fa.razor new file mode 100644 index 000000000000..e57bd5e84e1c --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWith2fa.razor @@ -0,0 +1,108 @@ +@page "/Account/LoginWith2fa" + +@using System.ComponentModel.DataAnnotations +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject NavigationManager NavigationManager +@inject ILogger Logger + +Two-factor authentication + +

Two-factor authentication

+
+ +

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

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

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

+ +@code { + private string? _message; + private ApplicationUser _user = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + [SupplyParameterFromQuery] + private bool RememberMe { get; set; } + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user is null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + _user = user; + } + + private async Task OnValidSubmitAsync() + { + var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty); + var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine); + var userId = await UserManager.GetUserIdAsync(_user); + + if (result.Succeeded) + { + Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", _user.Id); + NavigationManager.RedirectTo(ReturnUrl ?? "/"); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User with ID '{UserId}' account locked out.", _user.Id); + NavigationManager.RedirectTo("/Account/Lockout"); + } + else + { + Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", _user.Id); + _message = "Error: Invalid authenticator code."; + } + } + + private sealed class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string? TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + } +} diff --git a/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/Pages/Account/LoginWithRecoveryCode.razor new file mode 100644 index 000000000000..d6426e907be6 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/LoginWithRecoveryCode.razor @@ -0,0 +1,93 @@ +@page "/Account/LoginWithRecoveryCode" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Mvc +@using BlazorWeb_CSharp.Data + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject NavigationManager NavigationManager +@inject ILogger Logger + +Recovery code verification + +

Recovery code verification

+
+ +

+ You have requested to log in with a recovery code. This login will not be remembered until you provide + an authenticator app code at log in or disable 2FA and log in again. +

+
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + private string? _message; + private ApplicationUser _user = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + [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; + } + + private async Task OnValidSubmitAsync() + { + var recoveryCode = Input.RecoveryCode!.Replace(" ", string.Empty); + + var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + var userId = await UserManager.GetUserIdAsync(_user); + + if (result.Succeeded) + { + Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", _user.Id); + NavigationManager.RedirectTo(ReturnUrl ?? "/"); + } + if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + NavigationManager.RedirectTo("/Account/Lockout"); + } + else + { + Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", _user.Id); + _message = "Error: Invalid recovery code entered."; + } + } + + private sealed class InputModel + { + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string? RecoveryCode { get; set; } + } +} diff --git a/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/Pages/Account/Manage/ChangePassword.razor index 374ed75ae741..44af1e218c0f 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/Pages/Account/Manage/ChangePassword.razor @@ -6,6 +6,7 @@ @inject UserManager UserManager @inject SignInManager SignInManager +@inject UserAccessor UserAccessor @inject NavigationManager NavigationManager @inject ILogger Logger @@ -40,40 +41,27 @@ @code { private string? _message; - private ApplicationUser? _user; + private ApplicationUser _user = default!; private bool _hasPassword; [SupplyParameterFromForm] private InputModel Input { get; set; } = default!; - [CascadingParameter] - private Task? AuthenticationStateTask { get; set; } - protected override async Task OnInitializedAsync() { Input ??= new(); - (_user, _message) = await UserManager.GetUserAsync(AuthenticationStateTask); - if (_user is null) - { - return; - } - + _user = await UserAccessor.GetRequiredUserAsync(); _hasPassword = await UserManager.HasPasswordAsync(_user); if (!_hasPassword) { - NavigationManager.NavigateTo("/Account/Manage/SetPassword"); + NavigationManager.RedirectTo("/Account/Manage/SetPassword"); return; } } private async Task OnValidSubmitAsync() { - if (_user is null) - { - return; - } - var changePasswordResult = await UserManager.ChangePasswordAsync(_user, Input.OldPassword!, Input.NewPassword!); if (!changePasswordResult.Succeeded) { @@ -83,7 +71,8 @@ await SignInManager.RefreshSignInAsync(_user); Logger.LogInformation("User changed their password successfully."); - _message = "Your password has been changed."; + + NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Your password has been changed" }); } private sealed class InputModel 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/Pages/Account/Manage/DeletePersonalData.razor new file mode 100644 index 000000000000..b27a00c31520 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/DeletePersonalData.razor @@ -0,0 +1,84 @@ +@page "/Account/Manage/DeletePersonalData" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject UserAccessor UserAccessor +@inject NavigationManager NavigationManager +@inject ILogger Logger + +Delete Personal Data + + + +

Delete Personal Data

+ + + +
+ + + + @if (_requirePassword) + { +
+ + + +
+ } + +
+
+ +@code { + private string? _message; + private ApplicationUser _user = default!; + private bool _requirePassword; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + _user = await UserAccessor.GetRequiredUserAsync(); + _requirePassword = await UserManager.HasPasswordAsync(_user); + } + + private async Task OnValidSubmitAsync() + { + if (_requirePassword && !await UserManager.CheckPasswordAsync(_user, Input.Password!)) + { + _message = "Error: Incorrect password."; + return; + } + + var result = await UserManager.DeleteAsync(_user); + var userId = await UserManager.GetUserIdAsync(_user); + if (!result.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred deleting user."); + } + + await SignInManager.SignOutAsync(); + + Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + NavigationManager.RedirectToCurrentPage(); + } + + private sealed class InputModel + { + [DataType(DataType.Password)] + 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/Pages/Account/Manage/Disable2fa.razor new file mode 100644 index 000000000000..74a7b64fe7c0 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/Disable2fa.razor @@ -0,0 +1,67 @@ +@page "/Account/Manage/Disable2fa" + +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data + +@inject UserManager UserManager +@inject UserAccessor UserAccessor +@inject NavigationManager NavigationManager +@inject ILogger Logger + +Disable two-factor authentication (2FA) + + +

Disable two-factor authentication (2FA)

+ + + +
+
+ + + +
+ +@code { + private ApplicationUser _user = default!; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + _user = await UserAccessor.GetRequiredUserAsync(); + + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + if (!await UserManager.GetTwoFactorEnabledAsync(_user)) + { + throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled."); + } + return; + } + } + + private async Task OnSubmitAsync() + { + var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(_user, false); + if (!disable2faResult.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred disabling 2FA."); + } + + var userId = await UserManager.GetUserIdAsync(_user); + Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); + NavigationManager.RedirectTo( + "/Account/Manage/TwoFactorAuthentication", + new() { ["Message"] = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app" }); + } +} 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 index ab2a4b4cc536..a9f0ffb6d1a5 100644 --- 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 @@ -9,6 +9,7 @@ @using BlazorWeb_CSharp.Data @inject UserManager UserManager +@inject UserAccessor UserAccessor @inject IEmailSender EmailSender @inject NavigationManager NavigationManager @@ -16,7 +17,7 @@

Manage email

- +
@@ -54,27 +55,18 @@
@code { - private string? _message; - private ApplicationUser? _user; + private ApplicationUser _user = default!; private string? _email; private bool _isEmailConfirmed; [SupplyParameterFromForm] private InputModel Input { get; set; } = default!; - [CascadingParameter] - private Task? AuthenticationStateTask { get; set; } - protected override async Task OnInitializedAsync() { Input ??= new(); - (_user, _message) = await UserManager.GetUserAsync(AuthenticationStateTask); - if (_user is null) - { - return; - } - + _user = await UserAccessor.GetRequiredUserAsync(); _email = await UserManager.GetEmailAsync(_user); _isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(_user); @@ -83,49 +75,40 @@ private async Task OnValidSubmitAsync() { - if (_user is null) - { - return; - } - 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}Identity/Account/ConfirmEmailChange", + $"{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."); - _message = "Confirmation link to change email sent. Please check your email."; + + NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Confirmation link to change email sent. Please check your email." }); return; } - _message = "Your email is unchanged."; + NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Your email is unchanged." }); } private async Task OnSendEmailVerificationAsync() { - if (_user 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.BaseUri}Identity/Account/ConfirmEmail", + $"{NavigationManager.BaseUri}Account/ConfirmEmail", new Dictionary { { "userId", userId }, { "code", code } }); await EmailSender.SendEmailAsync( _email!, "Confirm your email", $"Please confirm your account by clicking here."); - _message = "Verification email sent. Please check your email."; + NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Verification email sent. Please check your email." }); } private sealed class InputModel 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/Pages/Account/Manage/EnableAuthenticator.razor new file mode 100644 index 000000000000..8efc21e1c061 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Manage/EnableAuthenticator.razor @@ -0,0 +1,168 @@ +@page "/Account/Manage/EnableAuthenticator" + +@using System.ComponentModel.DataAnnotations +@using System.Globalization +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data + +@inject UserManager UserManager +@inject UserAccessor UserAccessor +@inject UrlEncoder UrlEncoder +@inject NavigationManager NavigationManager +@inject ILogger Logger + +Configure authenticator app + + +

Configure authenticator app

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

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

    + +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    + + +
    + + + +
    + + +
    +
    +
    +
  6. +
+
+ +@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; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + _user = await UserAccessor.GetRequiredUserAsync(); + + await LoadSharedKeyAndQrCodeUriAsync(_user); + } + + private async Task OnValidSubmitAsync() + { + // Strip spaces and hyphens + var verificationCode = Input.Code!.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync( + _user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + await LoadSharedKeyAndQrCodeUriAsync(_user); + NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Error: Verification code is invalid." }); + return; + } + + await UserManager.SetTwoFactorEnabledAsync(_user, true); + var userId = await UserManager.GetUserIdAsync(_user); + Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); + + var message = "Your authenticator app has been verified."; + + if (await UserManager.CountRecoveryCodesAsync(_user) == 0) + { + var recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(_user, 10); + NavigationManager.RedirectTo( + "/Account/Manage/ShowRecoveryCodes", + new() { ["Message"] = message, ["RecoveryCodes"] = recoveryCodes?.ToArray() }); + } + else + { + NavigationManager.RedirectTo( + "/Account/Manage/TwoFactorAuthentication", + new() { ["Message"] = message }); + } + } + + private async ValueTask LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await UserManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); + } + + _sharedKey = FormatKey(unformattedKey!); + + var email = await UserManager.GetEmailAsync(user); + _authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' '); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + CultureInfo.InvariantCulture, + AuthenticatorUriFormat, + UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"), + UrlEncoder.Encode(email), + unformattedKey); + } + + private sealed class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string? Code { get; set; } + } +} diff --git a/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/Pages/Account/Manage/ExternalLogins.razor index 75c7538ad9d4..19e67d66c804 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/Pages/Account/Manage/ExternalLogins.razor @@ -1,7 +1,151 @@ @page "/Account/Manage/ExternalLogins" +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data +@using Microsoft.AspNetCore.Mvc.ViewFeatures + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject UserAccessor UserAccessor +@inject IUserStore UserStore +@inject NavigationManager NavigationManager + Manage your external logins -

Manage your external logins

+ +@if (_currentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in _currentLogins) + { + + + + + } + +
@login.ProviderDisplayName + @if (_showRemoveButton) + { + + +
+ + + +
+ + } + else + { + @:   + } +
+} +@if (_otherLogins?.Count > 0) +{ +

Add another service to log in.

+
+
@code { [SupplyParameterFromForm] - public InputModel Input { get; set; } = new(); + public InputModel Input { get; set; } = default!; /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used @@ -89,12 +66,6 @@ [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 IList? ExternalLogins { 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. @@ -133,9 +104,9 @@ IEnumerable? identityErrors; string? Message => identityErrors is null ? null : "Error: " + string.Join(", ", identityErrors.Select(error => error.Description)); - protected override async Task OnInitializedAsync() + protected override void OnInitialized() { - ExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + Input ??= new(); } public async Task RegisterUser(EditContext editContext) @@ -163,14 +134,14 @@ if (UserManager.Options.SignIn.RequireConfirmedAccount) { - NavigationManager.NavigateTo(NavigationManager.GetUriWithQueryParameters( - NavigationManager.ToAbsoluteUri("/Account/RegisterConfirmation").AbsoluteUri, - new Dictionary { { "email", Input.Email }, { "returnUrl", ReturnUrl } })); + NavigationManager.RedirectTo( + "/Account/RegisterConfirmation", + new() { ["Email"] = Input.Email, ["ReturnUrl"] = ReturnUrl }); } else { await SignInManager.SignInAsync(user, isPersistent: false); - NavigationManager.NavigateTo(NavigationManager.ToAbsoluteUri(NavigationManager.ToBaseRelativePath(ReturnUrl)).AbsoluteUri); + NavigationManager.RedirectTo(ReturnUrl); } } else 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 index e03daa4aaa18..4745cf7eda91 100644 --- 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 @@ -39,7 +39,7 @@ else { if (Email == null) { - NavigationManager.NavigateTo("/"); + NavigationManager.RedirectTo("/"); } else { 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/Pages/Account/ResendEmailConfirmation.razor new file mode 100644 index 000000000000..cfe0530a97d6 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ResendEmailConfirmation.razor @@ -0,0 +1,76 @@ +@page "/Account/ResendEmailConfirmation" + +@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 + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager + +Resend email confirmation + +

Resend email confirmation

+

Enter your email.

+
+ +
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + private string? _message; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + protected override void OnInitialized() + { + Input ??= new(); + } + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email!); + if (user is null) + { + _message = "Verification email sent. Please check your email."; + 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.BaseUri}Account/ConfirmEmail", + new Dictionary { ["userId"] = userId, ["code"] = code }); + await EmailSender.SendEmailAsync( + Input.Email!, + "Confirm your email", + $"Please confirm your account by clicking here."); + + _message = "Verification email sent. Please check your email."; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + 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/Pages/Account/ResetPassword.razor index 21a057e1bf4e..63492a9a43d9 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/Pages/Account/ResetPassword.razor @@ -57,7 +57,7 @@ { if (Code is null) { - NavigationManager.NavigateTo("/Account/InvalidPasswordReset"); + NavigationManager.RedirectTo("/Account/InvalidPasswordReset"); } else { @@ -71,14 +71,14 @@ if (user is null) { // Don't reveal that the user does not exist - NavigationManager.NavigateTo("/Account/ResetPasswordConfirmation"); + NavigationManager.RedirectTo("/Account/ResetPasswordConfirmation"); return; } var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); if (result.Succeeded) { - NavigationManager.NavigateTo("/Account/ResetPasswordConfirmation"); + NavigationManager.RedirectTo("/Account/ResetPasswordConfirmation"); return; } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor deleted file mode 100644 index 9dfc84b03c52..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/StatusMessage.razor +++ /dev/null @@ -1,12 +0,0 @@ -@if (!String.IsNullOrEmpty(Message)) -{ - var statusMessageClass = Message.StartsWith("Error") ? "danger" : "success"; - -} - -@code { - [Parameter] public string? Message { get; set; } -} \ No newline at end of file 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 new file mode 100644 index 000000000000..7399778dad7f --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserAccessor.cs @@ -0,0 +1,28 @@ +using BlazorWeb_CSharp.Data; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Identity; + +namespace BlazorWeb_CSharp.Data; + +internal sealed class UserAccessor( + IHttpContextAccessor httpContextAccessor, + UserManager userManager, + NavigationManager navigationManager) +{ + 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. + navigationManager.RedirectTo("/Account/InvalidUser", new() + { + ["Message"] = $"Error: Unable to load user with ID '{userManager.GetUserId(principal)}'.", + }); + } + + return user; + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs deleted file mode 100644 index 6d3808584a60..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Data/UserManagerExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Components.Authorization; - -namespace Microsoft.AspNetCore.Identity; - -internal static class UserManagerExtensions -{ - public static async Task<(TUser? User, string? Error)> GetUserAsync(this UserManager userManager, Task? authenticationStateTask) - where TUser : class - { - if (authenticationStateTask is null) - { - return (User: null, Error: "Unable to authenticate user."); - } - - var authenticationState = await authenticationStateTask; - var user = await userManager.GetUserAsync(authenticationState.User); - if (user is null) - { - return (User: null, Error: $"Unable to load user with ID '{userManager.GetUserId(authenticationState.User)}'."); - } - - return (User: user, Error: null); - } -} 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/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000000..7a24bbcc379a --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -0,0 +1,118 @@ +using System.Text.Json; +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; + +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. + public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var logger = endpoints.ServiceProvider.GetRequiredService>(); + var accountGroup = endpoints.MapGroup("/Account"); + + accountGroup.MapPost("/PerformExternalLogin", ( + HttpContext context, + [FromServices] SignInManager signInManager, + [FromForm] string provider, + [FromForm] string returnUrl) => + { + IEnumerable> query = [ + new("ReturnUrl", returnUrl), + new("Action", ExternalLogin.LoginCallbackAction)]; + + var redirectUrl = UriHelper.BuildRelative( + context.Request.PathBase, + $"/Account/ExternalLogin", + QueryString.Create(query)); + + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return Results.Challenge(properties, [provider]); + }); + + accountGroup.MapPost("/Logout", async ( + [FromServices] SignInManager signInManager, + [FromForm] string returnUrl) => + { + await signInManager.SignOutAsync(); + logger.LogInformation("User logged out."); + if (returnUrl is not null) + { + return Results.LocalRedirect(returnUrl); + } + else + { + // This needs to be a redirect so that the browser performs a new + // request and the identity for the user gets updated. + return Results.LocalRedirect("/"); + } + }); + + var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); + + manageGroup.MapPost("/LinkExternalLogin", async ( + HttpContext context, + [FromServices] SignInManager signInManager, + [FromForm] string provider) => + { + // Clear the existing external cookie to ensure a clean login process + await context.SignOutAsync(IdentityConstants.ExternalScheme); + + var redirectUrl = UriHelper.BuildRelative( + context.Request.PathBase, + $"/Account/Manage/ExternalLogins", + QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); + + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User)); + return Results.Challenge(properties, [provider]); + }); + + manageGroup.MapPost("/DownloadPersonalData", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] AuthenticationStateProvider authenticationStateProvider) => + { + var user = await userManager.GetUserAsync(context.User); + if (user is null) + { + return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); + } + + var userId = await userManager.GetUserIdAsync(user); + logger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId); + + // Only include personal data for download + var personalData = new Dictionary(); + var personalDataProps = typeof(ApplicationUser).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); + foreach (var p in personalDataProps) + { + personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); + } + + var logins = await userManager.GetLoginsAsync(user); + foreach (var l in logins) + { + personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); + } + + personalData.Add($"Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); + var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData); + + context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); + return Results.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); + }); + + return accountGroup; + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/RedirectNavigationManagerExtensions.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/RedirectNavigationManagerExtensions.cs new file mode 100644 index 000000000000..1cf481a1cd7c --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/RedirectNavigationManagerExtensions.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNetCore.Components; + +internal static class RedirectNavigationManagerExtensions +{ + // These extension methods help make more concise the common case of redirecting to the same or different page + // with a fresh set of query parameters. An exception is thrown if the current page is not being rendered from + // an endpoint, because it's not possible to redirect in that case. + + [DoesNotReturn] + public static void RedirectTo(this NavigationManager navigationManager, string 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 static void RedirectTo(this NavigationManager navigationManager, string uri, Dictionary queryParameters) + { + var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); + var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); + navigationManager.RedirectTo(newUri); + } + + [DoesNotReturn] + public static void RedirectToCurrentPage(this NavigationManager navigationManager) + => navigationManager.RedirectTo(navigationManager.Uri); + + [DoesNotReturn] + public static void RedirectToCurrentPage(this NavigationManager navigationManager, Dictionary queryParameters) + => navigationManager.RedirectTo(navigationManager.Uri, queryParameters); +} 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 7d805af202b7..52bf62ce50ae 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 @@ -39,6 +39,7 @@ public static void Main(string[] args) #if (IndividualLocalAuth) builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) .AddIdentityCookies(); @@ -102,6 +103,11 @@ public static void Main(string[] args) app.MapRazorComponents(); #endif + #if (IndividualLocalAuth) + // Add additional endpoints required by the Identity /Account Razor components. + app.MapAdditionalIdentityEndpoints(); + + #endif app.Run(); } } 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 857bf510ad44..c126786b65c3 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 @@ -33,6 +33,7 @@ #if (IndividualLocalAuth) builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) .AddIdentityCookies(); @@ -96,4 +97,9 @@ app.MapRazorComponents(); #endif +#if (IndividualLocalAuth) +// Add additional endpoints required by the Identity /Account Razor components. +app.MapAdditionalIdentityEndpoints(); + +#endif app.Run(); From 51240afb981556fd452ba12e4189556ee1ae451b Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 18 Sep 2023 12:21:34 -0700 Subject: [PATCH 03/18] PersistedAuthenticationStateProvider->PersistentAuthenticationStateProvider --- .../content/BlazorWeb-CSharp/.template.config/template.json | 2 +- ...teProvider.cs => PersistentAuthenticationStateProvider.cs} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/{PersistedAuthenticationStateProvider.cs => PersistentAuthenticationStateProvider.cs} (83%) 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 a49e8d59f673..8e9ceacc5aba 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 @@ -118,7 +118,7 @@ "BlazorWeb-CSharp/Data/**", "BlazorWeb-CSharp/Identity/**", "BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs", - "BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs", + "BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs", "BlazorWeb-CSharp.Client/UserInfo.cs", "BlazorWeb-CSharp.Client/Pages/Auth.razor" ] diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs similarity index 83% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs index f4e554f09a8b..8d1374e33cf9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs @@ -4,7 +4,7 @@ namespace BlazorWeb_CSharp.Client; -public class PersistedAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider +public class PersistentAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider { private static readonly Task _unauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); @@ -24,7 +24,7 @@ public override Task GetAuthenticationStateAsync() return Task.FromResult( new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, - authenticationType: nameof(PersistedAuthenticationStateProvider))))); + authenticationType: nameof(PersistentAuthenticationStateProvider))))); } } From 64c9fbb6c5e6ac19c63752e2e69bb20ae97a4081 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 18 Sep 2023 13:08:12 -0700 Subject: [PATCH 04/18] Handle !UseWebAssembly better --- .../.template.config/template.json | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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 8e9ceacc5aba..4656ab29a758 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 @@ -130,7 +130,7 @@ ] }, { - "condition": "(IndividualLocalAuth && UseLocalDB)", + "condition": "(IndividualLocalAuth && UseLocalDB && UseWebAssembly)", "rename": { "BlazorWeb-CSharp/Data/SqlServer/": "BlazorWeb-CSharp/Data/Migrations/" }, @@ -139,13 +139,31 @@ ] }, { - "condition": "(IndividualLocalAuth && !UseLocalDB)", + "condition": "(IndividualLocalAuth && UseLocalDB && !UseWebAssembly)", + "rename": { + "BlazorWeb-CSharp/Data/SqlServer/": "Data/Migrations/" + }, + "exclude": [ + "BlazorWeb-CSharp/Data/SqlLite/**" + ] + }, + { + "condition": "(IndividualLocalAuth && !UseLocalDB && UseWebAssembly)", "rename": { "BlazorWeb-CSharp/Data/SqlLite/": "BlazorWeb-CSharp/Data/Migrations/" }, "exclude": [ "BlazorWeb-CSharp/Data/SqlServer/**" ] + }, + { + "condition": "(IndividualLocalAuth && !UseLocalDB && !UseWebAssembly)", + "rename": { + "BlazorWeb-CSharp/Data/SqlLite/": "Data/Migrations/" + }, + "exclude": [ + "BlazorWeb-CSharp/Data/SqlServer/**" + ] } ] } From 2af81a05a3e4efbf3e2f6928531d9cdd8fb672dc Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 18 Sep 2023 15:08:39 -0700 Subject: [PATCH 05/18] Small fixes --- .../BlazorWeb-CSharp.Client/Pages/Auth.razor | 6 +++++- .../BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs | 2 +- .../Components/Pages/Account/LoginWith2fa.razor | 2 +- .../Components/Pages/Account/_Imports.razor | 1 + .../IdentityComponentsEndpointRouteBuilderExtensions.cs | 7 ++++--- 5 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/_Imports.razor diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor index e15f54ff654d..90021b5f507e 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/Auth.razor @@ -3,7 +3,11 @@ @using Microsoft.AspNetCore.Authorization; @attribute [Authorize] -@attribute [RenderModeAuto] +@*#if (UseServer && !InteractiveAtRoot) +@attribute [RenderModeInteractiveAuto] +##elseif (!InteractiveAtRoot) +@attribute [RenderModeInteractiveWebAssembly] +##endif*@ Auth diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs index 560bc77dbced..600e37d36537 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Program.cs @@ -9,7 +9,7 @@ #if (IndividualLocalAuth) builder.Services.AddAuthorizationCore(); builder.Services.AddCascadingAuthenticationState(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); #endif await builder.Build().RunAsync(); 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/Pages/Account/LoginWith2fa.razor index e57bd5e84e1c..3f818e07305d 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/Pages/Account/LoginWith2fa.razor @@ -42,7 +42,7 @@

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

@code { 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 new file mode 100644 index 000000000000..3856badae708 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/_Imports.razor @@ -0,0 +1 @@ +@using BlazorWeb_CSharp.Components.Identity 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/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs index 7a24bbcc379a..b8bc769bec97 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/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -18,7 +18,8 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn { ArgumentNullException.ThrowIfNull(endpoints); - var logger = endpoints.ServiceProvider.GetRequiredService>(); + var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("IdentityUI"); var accountGroup = endpoints.MapGroup("/Account"); accountGroup.MapPost("/PerformExternalLogin", ( @@ -35,7 +36,7 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn context.Request.PathBase, $"/Account/ExternalLogin", QueryString.Create(query)); - + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return Results.Challenge(properties, [provider]); }); @@ -72,7 +73,7 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn context.Request.PathBase, $"/Account/Manage/ExternalLogins", QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); - + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User)); return Results.Challenge(properties, [provider]); }); From e404b7d8965a558b1480b4547af621ca9b84c596 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 18 Sep 2023 15:37:14 -0700 Subject: [PATCH 06/18] Fix state persistence callback render mode --- .../PersistingAuthenticationStateProvider.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs index 362b62609fc9..f0b7b5405f0d 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs @@ -1,6 +1,7 @@ using BlazorWeb_CSharp.Client; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; @@ -35,11 +36,17 @@ public PersistingAuthenticationStateProvider(IHttpContextAccessor contextAccesso } return Task.CompletedTask; - }); + #if (UseServer && UseWebAssembly) + }, RenderMode.InteractiveAuto); + #elif (UseServer) + }, RenderMode.InteractiveServer); + #elif (UseWebAssembly) + }, RenderMode.InteractiveWebAssembly); + #endif } - private HttpContext RequiredHttpContext => - _contextAccessor.HttpContext ?? throw new InvalidOperationException("IHttpContextAccessor HttpContext AsyncLocal missing!"); + private HttpContext RequiredHttpContext => + _contextAccessor.HttpContext ?? throw new InvalidOperationException("IHttpContextAccessor HttpContext AsyncLocal missing!"); public override Task GetAuthenticationStateAsync() => Task.FromResult(new AuthenticationState(RequiredHttpContext.User)); From 3b103a7990a6025776920b4163375ddadf6f6398 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 18 Sep 2023 17:26:13 -0700 Subject: [PATCH 07/18] Logout functionality --- .../Components/Identity/LogoutForm.razor | 32 +++++++++++++++++++ .../Components/Layout/NavMenu.razor | 12 ++++++- .../Components/Layout/NavMenu.razor.css | 4 +++ ...omponentsEndpointRouteBuilderExtensions.cs | 18 ----------- 4 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/LogoutForm.razor 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 new file mode 100644 index 000000000000..71b82c8c35e5 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Identity/LogoutForm.razor @@ -0,0 +1,32 @@ +@using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data + +@inject SignInManager SignInManager +@inject NavigationManager NavigationManager + +
+ + + + +@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(); + NavigationManager.RedirectTo(ReturnUrl ?? "/"); + } + } +} 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 f9535bd7cb57..fafa8932ef55 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,4 +1,8 @@ -

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

@code { @@ -61,6 +61,7 @@ protected override async Task OnInitializedAsync() { Input ??= new(); + ReturnUrl ??= "/"; var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); if (user is null) @@ -80,12 +81,12 @@ if (result.Succeeded) { Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", _user.Id); - NavigationManager.RedirectTo(ReturnUrl ?? "/"); + RedirectManager.RedirectTo(ReturnUrl ?? "/"); } else if (result.IsLockedOut) { Logger.LogWarning("User with ID '{UserId}' account locked out.", _user.Id); - NavigationManager.RedirectTo("/Account/Lockout"); + RedirectManager.RedirectTo("/Account/Lockout"); } else { 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/Pages/Account/LoginWithRecoveryCode.razor index 67696e101c45..41d5d3660810 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/Pages/Account/LoginWithRecoveryCode.razor @@ -4,10 +4,11 @@ @using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Mvc @using BlazorWeb_CSharp.Data +@using BlazorWeb_CSharp.Identity @inject SignInManager SignInManager @inject UserManager UserManager -@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager @inject ILogger Logger Recovery code verification @@ -69,12 +70,12 @@ if (result.Succeeded) { Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", _user.Id); - NavigationManager.RedirectTo(ReturnUrl ?? "/"); + RedirectManager.RedirectTo(ReturnUrl ?? "/"); } if (result.IsLockedOut) { Logger.LogWarning("User account locked out."); - NavigationManager.RedirectTo("/Account/Lockout"); + RedirectManager.RedirectTo("/Account/Lockout"); } else { 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/Pages/Account/Manage/ChangePassword.razor index 68b4b2f13b81..aabf71983a62 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/Pages/Account/Manage/ChangePassword.razor @@ -3,11 +3,12 @@ @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 NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager @inject ILogger Logger Change password @@ -55,7 +56,7 @@ _hasPassword = await UserManager.HasPasswordAsync(_user); if (!_hasPassword) { - NavigationManager.RedirectTo("/Account/Manage/SetPassword"); + RedirectManager.RedirectTo("/Account/Manage/SetPassword"); return; } } @@ -72,7 +73,7 @@ await SignInManager.RefreshSignInAsync(_user); Logger.LogInformation("User changed their password successfully."); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Your password has been changed" }); + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed"); } private sealed class InputModel 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/Pages/Account/Manage/DeletePersonalData.razor index 0c5f1a4334c9..c3ed9c38635a 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/Pages/Account/Manage/DeletePersonalData.razor @@ -3,11 +3,12 @@ @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 NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager @inject ILogger Logger Delete Personal Data @@ -73,7 +74,7 @@ Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); - NavigationManager.RedirectToCurrentPage(); + RedirectManager.RedirectToCurrentPage(); } private sealed class InputModel 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/Pages/Account/Manage/Disable2fa.razor index 74a7b64fe7c0..562b5ca26577 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/Pages/Account/Manage/Disable2fa.razor @@ -2,10 +2,11 @@ @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data +@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject UserAccessor UserAccessor -@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager @inject ILogger Logger Disable two-factor authentication (2FA) @@ -60,8 +61,8 @@ var userId = await UserManager.GetUserIdAsync(_user); Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); - NavigationManager.RedirectTo( + RedirectManager.RedirectToWithStatus( "/Account/Manage/TwoFactorAuthentication", - new() { ["Message"] = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app" }); + "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"); } } 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 index 305210b7dcd0..c629d0a8ee8e 100644 --- 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 @@ -7,11 +7,13 @@ @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 @@ -88,11 +90,11 @@ "Confirm your email", $"Please confirm your account by clicking here."); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Confirmation link to change email sent. Please check your email." }); + RedirectManager.RedirectToCurrentPageWithStatus("Confirmation link to change email sent. Please check your email."); return; } - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Your email is unchanged." }); + RedirectManager.RedirectToCurrentPageWithStatus("Your email is unchanged."); } private async Task OnSendEmailVerificationAsync() @@ -108,7 +110,7 @@ "Confirm your email", $"Please confirm your account by clicking here."); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Verification email sent. Please check your email." }); + RedirectManager.RedirectToCurrentPageWithStatus("Verification email sent. Please check your email."); } private sealed class InputModel 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/Pages/Account/Manage/EnableAuthenticator.razor index 0427e3ffd860..52b3f0d68c5a 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/Pages/Account/Manage/EnableAuthenticator.razor @@ -6,18 +6,19 @@ @using System.Text.Encodings.Web @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data +@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject UserAccessor UserAccessor @inject UrlEncoder UrlEncoder -@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager @inject ILogger Logger Configure authenticator app @if (_recoveryCodes is not null) { - + } else { @@ -74,7 +75,7 @@ else private string? _authenticatorUri; private IEnumerable? _recoveryCodes; - private string? _statusMessage; + private string? _message; [SupplyParameterFromForm] private InputModel Input { get; set; } = default!; @@ -99,7 +100,7 @@ else if (!is2faTokenValid) { await LoadSharedKeyAndQrCodeUriAsync(_user); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Error: Verification code is invalid." }); + RedirectManager.RedirectToCurrentPageWithStatus("Error: Verification code is invalid."); return; } @@ -107,7 +108,7 @@ else var userId = await UserManager.GetUserIdAsync(_user); Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); - _statusMessage = "Your authenticator app has been verified."; + _message = "Your authenticator app has been verified."; if (await UserManager.CountRecoveryCodesAsync(_user) == 0) { @@ -115,9 +116,7 @@ else } else { - NavigationManager.RedirectTo( - "/Account/Manage/TwoFactorAuthentication", - new() { ["Message"] = _statusMessage }); + RedirectManager.RedirectToWithStatus("/Account/Manage/TwoFactorAuthentication", _message); } } 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/Pages/Account/Manage/ExternalLogins.razor index 19e67d66c804..4ff1ec38044a 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/Pages/Account/Manage/ExternalLogins.razor @@ -2,14 +2,15 @@ @using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Identity -@using BlazorWeb_CSharp.Data @using Microsoft.AspNetCore.Mvc.ViewFeatures +@using BlazorWeb_CSharp.Data +@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager @inject UserAccessor UserAccessor @inject IUserStore UserStore -@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager Manage your external logins @@ -118,12 +119,12 @@ var result = await UserManager.RemoveLoginAsync(_user, LoginProvider!, ProviderKey!); if (!result.Succeeded) { - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "The external login was not removed." }); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was not removed."); return; } await SignInManager.RefreshSignInAsync(_user); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "The external login was removed." }); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed."); } private async Task OnGetLinkLoginCallbackAsync() @@ -132,20 +133,20 @@ var info = await SignInManager.GetExternalLoginInfoAsync(userId); if (info == null) { - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Unexpected error occurred loading external login info." }); + RedirectManager.RedirectToCurrentPageWithStatus("Unexpected error occurred loading external login info."); return; } var result = await UserManager.AddLoginAsync(_user, info); if (!result.Succeeded) { - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "The external login was not added. External logins can only be associated with one account." }); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was not added. External logins can only be associated with one account."); return; } // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "The external login was added." }); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was added."); } } 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/Pages/Account/Manage/GenerateRecoveryCodes.razor index 8adcae6739b1..3de6f69e9c40 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/Pages/Account/Manage/GenerateRecoveryCodes.razor @@ -2,17 +2,18 @@ @using Microsoft.AspNetCore.Identity @using BlazorWeb_CSharp.Data +@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject UserAccessor UserAccessor -@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager @inject ILogger Logger Generate two-factor authentication (2FA) recovery codes @if (_recoveryCodes is not null) { - + } else { @@ -42,7 +43,7 @@ else private ApplicationUser _user = default!; private IEnumerable? _recoveryCodes; - private string? _statusMessage; + private string? _message; protected override async Task OnInitializedAsync() { @@ -59,7 +60,7 @@ else { var userId = await UserManager.GetUserIdAsync(_user); _recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(_user, 10); - _statusMessage = "You have generated new recovery codes."; + _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/Pages/Account/Manage/Index.razor index 6c7c87e5f8b5..7e59b2732987 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/Pages/Account/Manage/Index.razor @@ -4,12 +4,13 @@ @using System.Security.Claims @using Microsoft.AspNetCore.Identity; @using BlazorWeb_CSharp.Data; +@using BlazorWeb_CSharp.Identity @inject AuthenticationStateProvider AuthenticationStateProvider @inject UserManager UserManager @inject SignInManager SignInManager @inject UserAccessor UserAccessor; -@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager Profile @@ -61,13 +62,13 @@ var setPhoneResult = await UserManager.SetPhoneNumberAsync(_user, Input.PhoneNumber); if (!setPhoneResult.Succeeded) { - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Unexpected error when trying to set phone number." }); + RedirectManager.RedirectToCurrentPageWithStatus("Unexpected error when trying to set phone number."); return; } } await SignInManager.RefreshSignInAsync(_user); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Your profile has been updated" }); + RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated"); } private sealed class InputModel 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/Pages/Account/Manage/ResetAuthenticator.razor index f34634c42067..dd951c7d2410 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/Pages/Account/Manage/ResetAuthenticator.razor @@ -1,12 +1,13 @@ @page "/Account/Manage/ResetAuthenticator" -@using BlazorWeb_CSharp.Data @using Microsoft.AspNetCore.Identity +@using BlazorWeb_CSharp.Data +@using BlazorWeb_CSharp.Identity @inject UserManager UserManager @inject SignInManager SignInManager @inject UserAccessor UserAccessor -@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager @inject ILogger Logger Reset authenticator key @@ -47,8 +48,8 @@ await SignInManager.RefreshSignInAsync(_user); - NavigationManager.RedirectTo( + RedirectManager.RedirectToWithStatus( "/Account/Manage/EnableAuthenticator", - new() { ["Message"] = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key." }); + "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key."); } } 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/Pages/Account/Manage/SetPassword.razor index f247ab21440f..5f1aff4403ad 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/Pages/Account/Manage/SetPassword.razor @@ -3,11 +3,12 @@ @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 NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager Set password @@ -53,7 +54,7 @@ var hasPassword = await UserManager.HasPasswordAsync(_user); if (hasPassword) { - NavigationManager.RedirectTo("/Account/Manage/ChangePassword"); + RedirectManager.RedirectTo("/Account/Manage/ChangePassword"); return; } } @@ -68,7 +69,7 @@ } await SignInManager.RefreshSignInAsync(_user); - NavigationManager.RedirectToCurrentPage(new() { ["Message"] = "Your password has been set." }); + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set."); } private sealed class InputModel 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 index 99c5f3ef9168..fbb2f32eddbd 100644 --- 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 @@ -1,13 +1,14 @@ @page "/Account/Manage/TwoFactorAuthentication" -@using BlazorWeb_CSharp.Data @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 NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager Two-factor authentication (2FA) @@ -94,9 +95,7 @@ else { await SignInManager.ForgetTwoFactorClientAsync(); - NavigationManager.RedirectToCurrentPage(new() - { - ["Message"] = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.", - }); + 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/Register.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/Register.razor index ba7a3f13d505..822a95b69460 100644 --- 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 @@ -3,11 +3,12 @@ @using System.ComponentModel.DataAnnotations @using System.Text @using System.Text.Encodings.Web -@using BlazorWeb_CSharp.Data @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 @@ -15,6 +16,7 @@ @inject ILogger Logger @inject IEmailSender EmailSender @inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager Register @@ -134,14 +136,14 @@ if (UserManager.Options.SignIn.RequireConfirmedAccount) { - NavigationManager.RedirectTo( + RedirectManager.RedirectTo( "/Account/RegisterConfirmation", new() { ["Email"] = Input.Email, ["ReturnUrl"] = ReturnUrl }); } else { await SignInManager.SignInAsync(user, isPersistent: false); - NavigationManager.RedirectTo(ReturnUrl); + RedirectManager.RedirectTo(ReturnUrl); } } else 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 index 4745cf7eda91..5f54beb59e9a 100644 --- 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 @@ -1,14 +1,16 @@ @page "/Account/RegisterConfirmation" @using System.Text -@using BlazorWeb_CSharp.Data @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 @@ -39,7 +41,7 @@ else { if (Email == null) { - NavigationManager.RedirectTo("/"); + RedirectManager.RedirectTo("/"); } else { 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/Pages/Account/ResendEmailConfirmation.razor index f1de72583703..fa5f9d1dbafe 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/Pages/Account/ResendEmailConfirmation.razor @@ -7,10 +7,12 @@ @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 Resend email confirmation 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/Pages/Account/ResetPassword.razor index 5e04d17eba0c..214c42643eb4 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/Pages/Account/ResetPassword.razor @@ -1,13 +1,14 @@ @page "/Account/ResetPassword" -@using System.ComponentModel.DataAnnotations; -@using System.Text; -@using Microsoft.AspNetCore.Http; -@using Microsoft.AspNetCore.Identity; -@using Microsoft.AspNetCore.WebUtilities; -@using BlazorWeb_CSharp.Data; - -@inject NavigationManager NavigationManager +@using System.ComponentModel.DataAnnotations +@using System.Text +@using Microsoft.AspNetCore.Http +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using BlazorWeb_CSharp.Data +@using BlazorWeb_CSharp.Identity + +@inject IdentityRedirectManager RedirectManager @inject UserManager UserManager Reset password @@ -57,7 +58,7 @@ { if (Code is null) { - NavigationManager.RedirectTo("/Account/InvalidPasswordReset"); + RedirectManager.RedirectTo("/Account/InvalidPasswordReset"); } else { @@ -71,14 +72,14 @@ if (user is null) { // Don't reveal that the user does not exist - NavigationManager.RedirectTo("/Account/ResetPasswordConfirmation"); + RedirectManager.RedirectTo("/Account/ResetPasswordConfirmation"); return; } var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); if (result.Succeeded) { - NavigationManager.RedirectTo("/Account/ResetPasswordConfirmation"); + RedirectManager.RedirectTo("/Account/ResetPasswordConfirmation"); return; } 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 index 7399778dad7f..f6fefe902c63 100644 --- 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 @@ -1,26 +1,24 @@ -using BlazorWeb_CSharp.Data; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Identity; +using BlazorWeb_CSharp.Identity; namespace BlazorWeb_CSharp.Data; internal sealed class UserAccessor( IHttpContextAccessor httpContextAccessor, UserManager userManager, - NavigationManager navigationManager) + 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. - navigationManager.RedirectTo("/Account/InvalidUser", new() - { - ["Message"] = $"Error: Unable to load user with ID '{userManager.GetUserId(principal)}'.", - }); + 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/Identity/Extensions/RedirectNavigationManagerExtensions.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/RedirectNavigationManagerExtensions.cs deleted file mode 100644 index 1cf481a1cd7c..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/Extensions/RedirectNavigationManagerExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNetCore.Components; - -internal static class RedirectNavigationManagerExtensions -{ - // These extension methods help make more concise the common case of redirecting to the same or different page - // with a fresh set of query parameters. An exception is thrown if the current page is not being rendered from - // an endpoint, because it's not possible to redirect in that case. - - [DoesNotReturn] - public static void RedirectTo(this NavigationManager navigationManager, string 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 static void RedirectTo(this NavigationManager navigationManager, string uri, Dictionary queryParameters) - { - var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); - var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); - navigationManager.RedirectTo(newUri); - } - - [DoesNotReturn] - public static void RedirectToCurrentPage(this NavigationManager navigationManager) - => navigationManager.RedirectTo(navigationManager.Uri); - - [DoesNotReturn] - public static void RedirectToCurrentPage(this NavigationManager navigationManager, Dictionary queryParameters) - => navigationManager.RedirectTo(navigationManager.Uri, queryParameters); -} 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 new file mode 100644 index 000000000000..2a1cccde8fc4 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Identity/IdentityRedirectManager.cs @@ -0,0 +1,63 @@ +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/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Program.Main.cs index 87e21015aac9..01d1f6af2312 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 @@ -44,6 +44,7 @@ public static void Main(string[] args) #if (IndividualLocalAuth) builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped(); + builder.Services.AddScoped(); #if (UseServer && UseWebAssembly) builder.Services.AddScoped(); #elif (UseServer) 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 3fcede5135f5..27af48798696 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 @@ -38,6 +38,7 @@ #if (IndividualLocalAuth) builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped(); +builder.Services.AddScoped(); #if (UseServer && UseWebAssembly) builder.Services.AddScoped(); #elif (UseServer) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index b25b2cc5124e..bcbe392b2faf 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -591,7 +591,7 @@ "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "Data/Migrations/ApplicationDbContextModelSnapshot.cs", "Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "Identity/Extensions/RedirectNavigationManagerExtensions.cs", + "Identity/IdentityRedirectManager.cs", "Properties/launchSettings.json", "wwwroot/app.css", "wwwroot/favicon.png", @@ -689,7 +689,7 @@ "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "Data/Migrations/ApplicationDbContextModelSnapshot.cs", "Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "Identity/Extensions/RedirectNavigationManagerExtensions.cs", + "Identity/IdentityRedirectManager.cs", "Identity/IdentityRevalidatingAuthenticationStateProvider.cs", "Properties/launchSettings.json", "wwwroot/app.css", @@ -761,7 +761,7 @@ "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "Data/Migrations/ApplicationDbContextModelSnapshot.cs", "Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "Identity/Extensions/RedirectNavigationManagerExtensions.cs", + "Identity/IdentityRedirectManager.cs", "Identity/IdentityRevalidatingAuthenticationStateProvider.cs", "Properties/launchSettings.json", "wwwroot/app.css", @@ -865,7 +865,7 @@ "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", "{ProjectName}/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "{ProjectName}/Identity/Extensions/RedirectNavigationManagerExtensions.cs", + "{ProjectName}/Identity/IdentityRedirectManager.cs", "{ProjectName}/Identity/PersistingServerAuthenticationStateProvider.cs", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", @@ -978,7 +978,7 @@ "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", "{ProjectName}/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "{ProjectName}/Identity/Extensions/RedirectNavigationManagerExtensions.cs", + "{ProjectName}/Identity/IdentityRedirectManager.cs", "{ProjectName}/Identity/PersistingRevalidatingAuthenticationStateProvider.cs", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", @@ -1230,7 +1230,7 @@ "{ProjectName}/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "{ProjectName}/Data/Migrations/ApplicationDbContextModelSnapshot.cs", "{ProjectName}/Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs", - "{ProjectName}/Identity/Extensions/RedirectNavigationManagerExtensions.cs", + "{ProjectName}/Identity/IdentityRedirectManager.cs", "{ProjectName}/Identity/PersistingRevalidatingAuthenticationStateProvider.cs", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", From 0a3c7ec56d10eedaca7610e5d32ed7109f903568 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 21 Sep 2023 17:11:45 -0700 Subject: [PATCH 18/18] React to Error page added by #50550 --- .../test/Templates.Tests/template-baselines.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index bcbe392b2faf..ae58eed078b9 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -552,6 +552,7 @@ "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", @@ -649,6 +650,7 @@ "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", "Components/Pages/Auth.razor", + "Components/Pages/Error.razor", "Components/Pages/Counter.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", @@ -721,6 +723,7 @@ "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", "Components/Pages/Auth.razor", + "Components/Pages/Error.razor", "Components/Pages/Counter.razor", "Components/Pages/Home.razor", "Components/Pages/Weather.razor", @@ -826,6 +829,7 @@ "{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", @@ -877,8 +881,8 @@ "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/UserInfo.cs", "{ProjectName}.Client/_Imports.razor", - "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Auth.razor", + "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/wwwroot/appsettings.Development.json", "{ProjectName}.Client/wwwroot/appsettings.json" ], @@ -939,6 +943,7 @@ "{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", @@ -990,8 +995,8 @@ "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/UserInfo.cs", "{ProjectName}.Client/_Imports.razor", - "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Auth.razor", + "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/wwwroot/appsettings.Development.json", "{ProjectName}.Client/wwwroot/appsettings.json" ], @@ -1192,6 +1197,7 @@ "{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",