From b824289173ee9b9cee8e2ff94c053a9ef7af28f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:35:15 +0300 Subject: [PATCH 1/2] EFCore Samples (#38) * EFCore Samples * Cleanup * More Cleanup * More Cleanup * EFCore Store Permission Fix & Finalized Samples --- UltimateAuth.slnx | 5 + .../Brand/UAuthLogo.razor | 19 + .../Brand/UAuthLogo.razor.cs | 54 ++ .../Brand/UAuthLogoVariant.cs | 7 + ...UltimateAuth.Sample.UAuthHub.EFCore.csproj | 34 + .../Components/App.razor | 29 + .../Components/Layout/MainLayout.razor | 101 +++ .../Components/Layout/MainLayout.razor.cs | 130 ++++ .../Components/Layout/ReconnectModal.razor | 31 + .../Layout/ReconnectModal.razor.css | 157 ++++ .../Components/Layout/ReconnectModal.razor.js | 63 ++ .../Components/Pages/Home.razor | 103 +++ .../Components/Pages/Home.razor.cs | 261 +++++++ .../Components/Pages/NotAuthorized.razor | 27 + .../Components/Pages/NotAuthorized.razor.cs | 15 + .../Components/Pages/NotFound.razor | 5 + .../Components/Routes.razor | 73 ++ .../Components/_Imports.razor | 17 + .../Infrastructure/DarkModeManager.cs | 45 ++ ...0260412205559_InitUltimateAuth.Designer.cs | 716 ++++++++++++++++++ .../20260412205559_InitUltimateAuth.cs | 558 ++++++++++++++ .../Migrations/UAuthDbContextModelSnapshot.cs | 713 +++++++++++++++++ .../Program.cs | 93 +++ .../Properties/launchSettings.json | 23 + .../Seed/UAuthDbInitializer.cs | 46 ++ .../appsettings.Development.json | 8 + .../appsettings.json | 9 + .../uauthhub.db | Bin 0 -> 4096 bytes .../uauthhub.db-shm | Bin 0 -> 32768 bytes .../uauthhub.db-wal | Bin 0 -> 1211312 bytes .../wwwroot/UltimateAuth-Logo.png | Bin 0 -> 14776 bytes .../wwwroot/app.css | 148 ++++ ...deBeam.UltimateAuth.Sample.UAuthHub.csproj | 41 +- .../Brand/UAuthLogo.razor | 19 + .../Brand/UAuthLogo.razor.cs | 54 ++ .../Brand/UAuthLogoVariant.cs | 7 + ...odeBeam.UAuth.Sample.IntWasm.Client.csproj | 24 + .../Common/UAuthDialog.cs | 29 + .../Custom/UAuthPageComponent.razor | 10 + .../Dialogs/AccountStatusDialog.razor | 23 + .../Dialogs/AccountStatusDialog.razor.cs | 77 ++ .../Components/Dialogs/CreateUserDialog.razor | 27 + .../Dialogs/CreateUserDialog.razor.cs | 55 ++ .../Components/Dialogs/CredentialDialog.razor | 51 ++ .../Dialogs/CredentialDialog.razor.cs | 92 +++ .../Components/Dialogs/IdentifierDialog.razor | 115 +++ .../Dialogs/IdentifierDialog.razor.cs | 311 ++++++++ .../Components/Dialogs/PermissionDialog.razor | 46 ++ .../Dialogs/PermissionDialog.razor.cs | 120 +++ .../Components/Dialogs/ProfileDialog.razor | 103 +++ .../Components/Dialogs/ProfileDialog.razor.cs | 116 +++ .../Components/Dialogs/ResetDialog.razor | 38 + .../Components/Dialogs/ResetDialog.razor.cs | 42 + .../Dialogs/ResourceApiDialog.razor | 54 ++ .../Dialogs/ResourceApiDialog.razor.cs | 95 +++ .../Components/Dialogs/RoleDialog.razor | 90 +++ .../Components/Dialogs/RoleDialog.razor.cs | 176 +++++ .../Components/Dialogs/SessionDialog.razor | 226 ++++++ .../Components/Dialogs/SessionDialog.razor.cs | 286 +++++++ .../Components/Dialogs/UserDetailDialog.razor | 75 ++ .../Dialogs/UserDetailDialog.razor.cs | 100 +++ .../Components/Dialogs/UserRoleDialog.razor | 49 ++ .../Dialogs/UserRoleDialog.razor.cs | 124 +++ .../Components/Dialogs/UsersDialog.razor | 94 +++ .../Components/Dialogs/UsersDialog.razor.cs | 188 +++++ .../Infrastructure/DarkModeManager.cs | 45 ++ .../Layout/MainLayout.razor | 72 ++ .../Layout/MainLayout.razor.cs | 130 ++++ .../Pages/AnonymousTestPage.razor | 1 + .../Pages/AuthorizedTestPage.razor | 26 + .../Pages/Home.razor | 455 +++++++++++ .../Pages/Home.razor.cs | 227 ++++++ .../Pages/LandingPage.razor | 4 + .../Pages/LandingPage.razor.cs | 17 + .../Pages/Login.razor | 135 ++++ .../Pages/Login.razor.cs | 216 ++++++ .../Pages/NotAuthorized.razor | 27 + .../Pages/NotAuthorized.razor.cs | 15 + .../Pages/Register.razor | 61 ++ .../Pages/Register.razor.cs | 45 ++ .../Pages/ResetCredential.razor | 18 + .../Pages/ResetCredential.razor.cs | 49 ++ .../Program.cs | 33 + .../ResourceApi/ProductApiService.cs | 78 ++ .../ResourceApi/SampleProduct.cs | 7 + .../Routes.razor | 73 ++ .../_Imports.razor | 19 + .../wwwroot/appsettings.Development.json | 8 + .../wwwroot/appsettings.json | 8 + .../CodeBeam.UAuth.Sample.IntWasm.csproj | 18 + .../Components/App.razor | 26 + .../Components/Pages/Error.razor | 36 + .../Components/_Imports.razor | 14 + .../CodeBeam.UAuth.Sample.IntWasm/Program.cs | 65 ++ .../Properties/launchSettings.json | 25 + .../appsettings.Development.json | 8 + .../appsettings.json | 9 + .../wwwroot/UltimateAuth-Logo.png | Bin 0 -> 14776 bytes .../wwwroot/app.css | 206 +++++ ...imateAuth.Sample.ResourceApi.EfCore.csproj | 17 + ...ltimateAuth.Sample.ResourceApi.EfCore.http | 6 + .../Program.cs | 28 + .../Properties/launchSettings.json | 23 + .../SampleData/AppActions.cs | 18 + .../SampleData/ProductStore.cs | 6 + .../SampleData/ProductsController.cs | 73 ++ .../SampleData/SampleProduct.cs | 7 + .../appsettings.Development.json | 8 + .../appsettings.json | 9 + .../Program.cs | 1 + .../Issuers/UAuthTokenIssuer.cs | 5 +- .../Stores/EfCoreRoleStore.cs | 23 +- ...Beam.UltimateAuth.Client.AspNetCore.csproj | 28 + .../NoOpAuthHandler.cs | 22 + .../README.md | 30 + .../ServiceCollectionExtensions.cs | 18 + .../uauthlogo.png | Bin 0 -> 4954 bytes 117 files changed, 8919 insertions(+), 26 deletions(-) create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogoVariant.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/App.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.css create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.js create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotFound.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Routes.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/_Imports.razor create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Infrastructure/DarkModeManager.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.Designer.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Properties/launchSettings.json create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Seed/UAuthDbInitializer.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.Development.json create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.json create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/wwwroot/UltimateAuth-Logo.png create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/wwwroot/app.css create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogoVariant.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Common/UAuthDialog.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Custom/UAuthPageComponent.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Infrastructure/DarkModeManager.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AnonymousTestPage.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/ProductApiService.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/SampleProduct.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Routes.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/_Imports.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.Development.json create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.json create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/Pages/Error.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/_Imports.razor create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Properties/launchSettings.json create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.Development.json create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.json create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/UltimateAuth-Logo.png create mode 100644 samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/app.css create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.http create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Program.cs create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Properties/launchSettings.json create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/AppActions.cs create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductStore.cs create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductsController.cs create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/SampleProduct.cs create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.Development.json create mode 100644 samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.json create mode 100644 src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj create mode 100644 src/client/CodeBeam.UltimateAuth.Client.AspNetCore/NoOpAuthHandler.cs create mode 100644 src/client/CodeBeam.UltimateAuth.Client.AspNetCore/README.md create mode 100644 src/client/CodeBeam.UltimateAuth.Client.AspNetCore/ServiceCollectionExtensions.cs create mode 100644 src/client/CodeBeam.UltimateAuth.Client.AspNetCore/uauthlogo.png diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx index c449693f..fb479af0 100644 --- a/UltimateAuth.slnx +++ b/UltimateAuth.slnx @@ -13,7 +13,11 @@ + + + + @@ -35,6 +39,7 @@ + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor new file mode 100644 index 00000000..2806b7d3 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor @@ -0,0 +1,19 @@ +@namespace CodeBeam.UltimateAuth.Sample +@inherits ComponentBase + + + + @if (Variant == UAuthLogoVariant.Brand) + { + + + + } + else + { + + + + } + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor.cs new file mode 100644 index 00000000..030d9b66 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace CodeBeam.UltimateAuth.Sample; + +public partial class UAuthLogo : ComponentBase +{ + [Parameter] public UAuthLogoVariant Variant { get; set; } = UAuthLogoVariant.Brand; + + [Parameter] public int Size { get; set; } = 32; + + [Parameter] public string? ShieldColor { get; set; } = "#00072d"; + [Parameter] public string? KeyColor { get; set; } = "#f6f5ae"; + + [Parameter] public string? Class { get; set; } + [Parameter] public string? Style { get; set; } + + private string BuildStyle() + { + if (Variant == UAuthLogoVariant.Mono) + return $"color: {KeyColor}; {Style}"; + + return Style ?? ""; + } + + protected string KeyPath => @" +M120.43,39.44H79.57A11.67,11.67,0,0,0,67.9,51.11V77.37 +A11.67,11.67,0,0,0,79.57,89H90.51l3.89,3.9v5.32l-3.8,3.81v81.41H99 +v-5.33h13.69V169H108.1v-3.8H99C99,150.76,111.9,153,111.9,153 +V99.79h-8V93.32L108.19,89h12.24 +A11.67,11.67,0,0,0,132.1,77.37V51.11 +A11.67,11.67,0,0,0,120.43,39.44Z + +M79.57,48.19h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.84a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.84a2.91,2.91 0 0 1 2.91,-2.92Z + +M79.57,68.62h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.83a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.83a2.91,2.91 0 0 1 2.91,-2.92Z + +M114.59,48.19h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.84a2.91,2.91 0 0 1 -2.91,2.91 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.91 +v-5.84a2.92,2.92 0 0 1 2.92,-2.92Z + +M114.59,68.62h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.83a2.91,2.91 0 0 1 -2.91,2.92 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.92 +v-5.83a2.92,2.92 0 0 1 2.92,-2.92Z +"; +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogoVariant.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogoVariant.cs new file mode 100644 index 00000000..fe3be220 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogoVariant.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample; + +public enum UAuthLogoVariant +{ + Brand, + Mono +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj new file mode 100644 index 00000000..f79f1a74 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/App.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/App.razor new file mode 100644 index 00000000..3156b071 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/App.razor @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor new file mode 100644 index 00000000..ac680f3b --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor @@ -0,0 +1,101 @@ +@inherits UAuthHubLayoutComponentBase +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject NavigationManager Nav + +@if (!IsHubAuthorized) +{ + + + UltimateAuth + + UAuthHub Sample + + + + + + + + + + + + Access Denied + + + This page cannot be accessed directly. + UAuthHub login flows can only be initiated by an authorized client application. + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + + return; +} + + + + + UltimateAuth + + UAuthHub Sample + + + + + + + + + +
+ + + @((state.Identity?.DisplayName ?? "?").Trim() is var n ? (n.Length >= 2 ? n[..2] : n[..1]) : "?") + + +
+
+ + + @state.Identity?.DisplayName + @string.Join(", ", state.Claims.Roles) + + + + + + + + @if (state.Identity?.SessionState is not null && state.Identity.SessionState != SessionState.Active) + { + + + } + +
+
+ + + + +
+
+ + + @Body + +
+ + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor.cs new file mode 100644 index 00000000..e5886028 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor.cs @@ -0,0 +1,130 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Infrastructure; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components.Layout; + +public partial class MainLayout +{ + [CascadingParameter] + public UAuthState UAuth { get; set; } = default!; + + [CascadingParameter] + public DarkModeManager DarkModeManager { get; set; } = default!; + + private async Task Refresh() + { + await UAuthClient.Flows.RefreshAsync(); + } + + private async Task Logout() + { + await UAuthClient.Flows.LogoutAsync(); + } + + private Color GetBadgeColor() + { + if (UAuth is null || !UAuth.IsAuthenticated) + return Color.Error; + + if (UAuth.IsStale) + return Color.Warning; + + var state = UAuth.Identity?.SessionState; + + if (state is null || state == SessionState.Active) + return Color.Success; + + if (state == SessionState.Invalid) + return Color.Error; + + return Color.Warning; + } + + private void HandleSignInClick() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login?focus=1", replace: true, forceLoad: true); + return; + } + + GoToLoginWithReturn(); + } + + private async Task Validate() + { + try + { + var result = await UAuthClient.Flows.ValidateAsync(); + + if (result.IsValid) + { + if (result.Snapshot?.Identity.UserStatus == UserStatus.SelfSuspended) + { + Snackbar.Add("Your account is suspended by you.", Severity.Warning); + return; + } + Snackbar.Add($"Session active • Tenant: {result.Snapshot?.Identity?.Tenant.Value} • User: {result.Snapshot?.Identity?.PrimaryUserName}", Severity.Success); + } + else + { + switch (result.State) + { + case SessionState.Expired: + Snackbar.Add("Session expired. Please sign in again.", Severity.Warning); + break; + + case SessionState.DeviceMismatch: + Snackbar.Add("Session invalid for this device.", Severity.Error); + break; + + default: + Snackbar.Add($"Session state: {result.State}", Severity.Error); + break; + } + } + } + catch (UAuthTransportException) + { + Snackbar.Add("Network error.", Severity.Error); + } + catch (UAuthProtocolException) + { + Snackbar.Add("Invalid response.", Severity.Error); + } + catch (UAuthException ex) + { + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Unexpected error: {ex.Message}", Severity.Error); + } + } + + private void GoToLoginWithReturn() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login", replace: true); + return; + } + + var current = Nav.ToBaseRelativePath(uri.ToString()); + if (string.IsNullOrWhiteSpace(current)) + current = "home"; + + var returnUrl = Uri.EscapeDataString("/" + current.TrimStart('/')); + Nav.NavigateTo($"/login?returnUrl={returnUrl}", replace: true); + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor new file mode 100644 index 00000000..e740b0c8 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor @@ -0,0 +1,31 @@ + + + +
+ +

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +

+ The session has been paused by the server. +

+

+ Failed to resume the session.
Please retry or reload the page. +

+ +
+
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.css b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.css new file mode 100644 index 00000000..3ad3773f --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.css @@ -0,0 +1,157 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-pause-visible, +.components-resume-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-paused .components-pause-visible, +#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] + +{ + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; +} + +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.js b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.js new file mode 100644 index 00000000..a44de78d --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor.js @@ -0,0 +1,63 @@ +// Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + +const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); + +const resumeButton = document.getElementById("components-resume-button"); +resumeButton.addEventListener("click", resume); + +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } else if (event.detail.state === "rejected") { + location.reload(); + } +} + +async function retry() { + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + const resumeSuccessful = await Blazor.resumeCircuit(); + if (!resumeSuccessful) { + location.reload(); + } else { + reconnectModal.close(); + } + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} + +async function resume() { + try { + const successful = await Blazor.resumeCircuit(); + if (!successful) { + location.reload(); + } + } catch { + reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed"); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === "visible") { + await retry(); + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor new file mode 100644 index 00000000..b1720a39 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor @@ -0,0 +1,103 @@ +@page "/" +@page "/login" +@attribute [UAuthLoginPage] +@inherits UAuthHubPageBase + +@implements IDisposable +@using CodeBeam.UltimateAuth.Client.Infrastructure +@using CodeBeam.UltimateAuth.Client.Options +@using CodeBeam.UltimateAuth.Client.Runtime +@using CodeBeam.UltimateAuth.Core.Abstractions +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Server.Services +@using CodeBeam.UltimateAuth.Server.Stores +@using Microsoft.Extensions.Options +@inject IUAuthClient UAuthClient +@inject IAuthStore AuthStore +@inject IHubFlowService HubFlowService +@inject IPkceService PkceService +@inject IHubCredentialResolver HubCredentialResolver +@inject IClientStorage BrowserStorage +@inject ISnackbar Snackbar +@inject IUAuthClientProductInfoProvider ClientProductInfoProvider +@inject IDeviceIdProvider DeviceIdProvider +@inject IDialogService DialogService +@inject IOptions Options + + + + + + + + + + + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor.cs new file mode 100644 index 00000000..0cc81888 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/Home.razor.cs @@ -0,0 +1,261 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Stores; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components.Pages; + +public partial class Home +{ + private string? _username; + private string? _password; + + private UAuthClientProductInfo? _productInfo; + private UAuthLoginForm _loginForm = null!; + + private CancellationTokenSource? _lockoutCts; + private PeriodicTimer? _lockoutTimer; + private DateTimeOffset? _lockoutUntil; + private TimeSpan _remaining; + private bool _isLocked; + private DateTimeOffset? _lockoutStartedAt; + private TimeSpan _lockoutDuration; + private double _progressPercent; + private int? _remainingAttempts = null; + private bool _errorHandled; + + protected override async Task OnInitializedAsync() + { + _productInfo = ClientProductInfoProvider.Get(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (string.IsNullOrWhiteSpace(HubKey)) + return; + + if (HubState is null || !HubState.Exists) + { + return; + } + + if (HubState.IsExpired) + { + await ContinuePkceAsync(); + return; + } + + if (HubState.Error != null && !_errorHandled) + { + _errorHandled = true; + Snackbar.Add(ResolveErrorMessage(HubState.Error), Severity.Error); + await ContinuePkceAsync(); + + if (HubSessionId.TryParse(HubKey, out var hubSessionId)) + { + await ReloadState(); + } + + await _loginForm.ReloadAsync(); + + StateHasChanged(); + } + } + + // For testing & debugging + private async Task ProgrammaticPkceLogin() + { + var hub = HubState; + + if (hub is null) + return; + + if (!HubSessionId.TryParse(HubKey, out var hubSessionId)) + return; + + var credentials = await HubCredentialResolver.ResolveAsync(hubSessionId); + + var request = new PkceCompleteRequest + { + Identifier = "admin", + Secret = "admin", + AuthorizationCode = credentials?.AuthorizationCode ?? string.Empty, + CodeVerifier = credentials?.CodeVerifier ?? string.Empty, + ReturnUrl = HubState?.ReturnUrl ?? string.Empty, + HubSessionId = HubState?.HubSessionId.Value ?? hubSessionId.Value, + }; + + await UAuthClient.Flows.TryCompletePkceLoginAsync(request, UAuthSubmitMode.TryAndCommit); + } + + private async Task HandleLoginResult(IUAuthTryResult result) + { + if (result is TryPkceLoginResult pkce) + { + if (!result.Success) + { + if (result.Reason == AuthFailureReason.LockedOut && result.LockoutUntilUtc is { } until) + { + _lockoutUntil = until; + StartCountdown(); + } + + _remainingAttempts = result.RemainingAttempts; + + ShowLoginError(result.Reason, result.RemainingAttempts); + await ContinuePkceAsync(); + } + } + } + + private HubCredentials? _pkce; + + private async Task ContinuePkceAsync() + { + if (string.IsNullOrWhiteSpace(HubKey)) + return; + + var key = new AuthArtifactKey(HubKey); + var artifact = await AuthStore.GetAsync(key) as HubFlowArtifact; + + if (artifact is null) + return; + + _pkce = await PkceService.RefreshAsync(artifact); + await HubFlowService.ContinuePkceAsync(HubKey, _pkce.AuthorizationCode, _pkce.CodeVerifier); + } + + private async Task StartNewPkceAsync() + { + var returnUrl = await ResolveReturnUrlAsync(); + await UAuthClient.Flows.BeginPkceAsync(returnUrl); + } + + private async Task ResolveReturnUrlAsync() + { + var fromContext = HubState?.ReturnUrl; + if (!string.IsNullOrWhiteSpace(fromContext)) + return fromContext; + + var uri = Nav.ToAbsoluteUri(Nav.Uri); + var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query); + + if (query.TryGetValue("return_url", out var ru) && !string.IsNullOrWhiteSpace(ru)) + return ru!; + + if (query.TryGetValue("hub", out var hubKey) && !string.IsNullOrWhiteSpace(hubKey)) + { + var artifact = await AuthStore.GetAsync(new AuthArtifactKey(hubKey!)); + if (artifact is HubFlowArtifact flow && !string.IsNullOrWhiteSpace(flow.ReturnUrl)) + return flow.ReturnUrl!; + } + + return Nav.Uri; + } + + private async void StartCountdown() + { + if (_lockoutUntil is null) + return; + + _isLocked = true; + _lockoutStartedAt = DateTimeOffset.UtcNow; + _lockoutDuration = _lockoutUntil.Value - DateTimeOffset.UtcNow; + UpdateRemaining(); + + _lockoutCts?.Cancel(); + _lockoutCts = new CancellationTokenSource(); + + _lockoutTimer?.Dispose(); + _lockoutTimer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + + try + { + while (await _lockoutTimer.WaitForNextTickAsync(_lockoutCts.Token)) + { + UpdateRemaining(); + + if (_remaining <= TimeSpan.Zero) + { + ResetLockoutState(); + await InvokeAsync(StateHasChanged); + break; + } + + await InvokeAsync(StateHasChanged); + } + } + catch (OperationCanceledException) + { + + } + } + + private void ResetLockoutState() + { + _isLocked = false; + _lockoutUntil = null; + _progressPercent = 0; + _remainingAttempts = null; + } + + private void UpdateRemaining() + { + if (_lockoutUntil is null || _lockoutStartedAt is null) + return; + + var now = DateTimeOffset.UtcNow; + + _remaining = _lockoutUntil.Value - now; + + if (_remaining <= TimeSpan.Zero) + { + _remaining = TimeSpan.Zero; + return; + } + + var elapsed = now - _lockoutStartedAt.Value; + + if (_lockoutDuration.TotalSeconds > 0) + { + var percent = 100 - (elapsed.TotalSeconds / _lockoutDuration.TotalSeconds * 100); + _progressPercent = Math.Max(0, percent); + } + } + + private void ShowLoginError(AuthFailureReason? reason, int? remainingAttempts) + { + string message = reason switch + { + AuthFailureReason.InvalidCredentials when remainingAttempts is > 0 + => $"Invalid username or password. {remainingAttempts} attempt(s) remaining.", + + AuthFailureReason.InvalidCredentials + => "Invalid username or password.", + + AuthFailureReason.RequiresMfa + => "Multi-factor authentication required.", + + AuthFailureReason.LockedOut + => "Your account is locked.", + + _ => "Login failed." + }; + + Snackbar.Add(message, Severity.Error); + } + + private string ResolveErrorMessage(HubErrorCode? errorCode) + { + if (errorCode == HubErrorCode.InvalidCredentials) + { + return "Invalid credentials."; + } + + return "Failed attempt."; + } + +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor new file mode 100644 index 00000000..2c0e9b77 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor @@ -0,0 +1,27 @@ +@inject NavigationManager Nav + + + + + + + Access Denied + + + You don’t have permission to view this page. + If you think this is a mistake, sign in with a different account or request access. + + + + Sign In + Go Back + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor.cs new file mode 100644 index 00000000..9d96c733 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotAuthorized.razor.cs @@ -0,0 +1,15 @@ +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components.Pages; + +public partial class NotAuthorized +{ + private string LoginHref + { + get + { + var returnUrl = Uri.EscapeDataString(Nav.ToBaseRelativePath(Nav.Uri)); + return $"/login?returnUrl=/{returnUrl}"; + } + } + + private void GoBack() => Nav.NavigateTo("/", replace: false); +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotFound.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotFound.razor new file mode 100644 index 00000000..917ada1d --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Pages/NotFound.razor @@ -0,0 +1,5 @@ +@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Routes.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Routes.razor new file mode 100644 index 00000000..9d7b5207 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Routes.razor @@ -0,0 +1,73 @@ +@using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components.Pages +@using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Infrastructure +@inject ISnackbar Snackbar +@inject DarkModeManager DarkModeManager + + + + + + + + + + + + + + + + @* Advanced: you can fully control routing by providing your own Router *@ + @* + + + + + + + + + + + + + + + + *@ + + +@code { + private async Task HandleReauth() + { + Snackbar.Add("Reauthentication required. Please log in again.", Severity.Warning); + } + + #region DarkMode + + protected override void OnInitialized() + { + DarkModeManager.Changed += OnThemeChanged; + } + + private void OnThemeChanged() + { + InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await DarkModeManager.InitializeAsync(); + StateHasChanged(); + } + } + + public void Dispose() + { + DarkModeManager.Changed -= OnThemeChanged; + } + + #endregion +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/_Imports.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/_Imports.razor new file mode 100644 index 00000000..56f13ea9 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/_Imports.razor @@ -0,0 +1,17 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore +@using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components +@using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components.Layout +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.Client +@using CodeBeam.UltimateAuth.Client.Blazor + +@using MudBlazor +@using MudExtensions diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Infrastructure/DarkModeManager.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Infrastructure/DarkModeManager.cs new file mode 100644 index 00000000..70ab1af0 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Infrastructure/DarkModeManager.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Infrastructure; + +public sealed class DarkModeManager +{ + private const string StorageKey = "uauth:theme:dark"; + + private readonly IClientStorage _storage; + + public DarkModeManager(IClientStorage storage) + { + _storage = storage; + } + + public async Task InitializeAsync() + { + var value = await _storage.GetAsync(StorageScope.Local, StorageKey); + + if (bool.TryParse(value, out var parsed)) + IsDarkMode = parsed; + } + + public bool IsDarkMode { get; set; } + + public event Action? Changed; + + public async Task ToggleAsync() + { + IsDarkMode = !IsDarkMode; + + await _storage.SetAsync(StorageScope.Local, StorageKey, IsDarkMode.ToString()); + Changed?.Invoke(); + } + + public void Set(bool value) + { + if (IsDarkMode == value) + return; + + IsDarkMode = value; + Changed?.Invoke(); + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.Designer.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.Designer.cs new file mode 100644 index 00000000..96400072 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.Designer.cs @@ -0,0 +1,716 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20260412205559_InitUltimateAuth")] + partial class InitUltimateAuth + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.cs new file mode 100644 index 00000000..35dc371f --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20260412205559_InitUltimateAuth.cs @@ -0,0 +1,558 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + /// + public partial class InitUltimateAuth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UAuth_Authentication", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Scope = table.Column(type: "INTEGER", nullable: false), + CredentialType = table.Column(type: "INTEGER", nullable: true), + FailedAttempts = table.Column(type: "INTEGER", nullable: false), + LastFailedAt = table.Column(type: "TEXT", nullable: true), + LockedUntil = table.Column(type: "TEXT", nullable: true), + RequiresReauthentication = table.Column(type: "INTEGER", nullable: false), + ResetRequestedAt = table.Column(type: "TEXT", nullable: true), + ResetExpiresAt = table.Column(type: "TEXT", nullable: true), + ResetConsumedAt = table.Column(type: "TEXT", nullable: true), + ResetTokenHash = table.Column(type: "TEXT", maxLength: 512, nullable: true), + ResetAttempts = table.Column(type: "INTEGER", nullable: false), + SecurityVersion = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_Authentication", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_PasswordCredentials", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + SecretHash = table.Column(type: "TEXT", maxLength: 512, nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + ExpiresAt = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: false), + LastUsedAt = table.Column(type: "TEXT", nullable: true), + Source = table.Column(type: "TEXT", maxLength: 128, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_PasswordCredentials", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_RefreshTokens", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TokenId = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + TokenHash = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + SessionId = table.Column(type: "TEXT", nullable: false), + ChainId = table.Column(type: "TEXT", nullable: true), + ReplacedByTokenHash = table.Column(type: "TEXT", maxLength: 128, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ExpiresAt = table.Column(type: "TEXT", nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_RefreshTokens", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_RolePermissions", + columns: table => new + { + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false), + Permission = table.Column(type: "TEXT", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_RolePermissions", x => new { x.Tenant, x.RoleId, x.Permission }); + }); + + migrationBuilder.CreateTable( + name: "UAuth_Roles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + NormalizedName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_SessionRoots", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RootId = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + RevokedAt = table.Column(type: "TEXT", nullable: true), + SecurityVersion = table.Column(type: "INTEGER", nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_SessionRoots", x => x.Id); + table.UniqueConstraint("AK_UAuth_SessionRoots_Tenant_RootId", x => new { x.Tenant, x.RootId }); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserIdentifiers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + Value = table.Column(type: "TEXT", maxLength: 256, nullable: false), + NormalizedValue = table.Column(type: "TEXT", maxLength: 256, nullable: false), + IsPrimary = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + VerifiedAt = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserIdentifiers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserLifecycles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + SecurityVersion = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserLifecycles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserProfiles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ProfileKey = table.Column(type: "TEXT", maxLength: 64, nullable: false), + FirstName = table.Column(type: "TEXT", nullable: true), + LastName = table.Column(type: "TEXT", nullable: true), + DisplayName = table.Column(type: "TEXT", nullable: true), + BirthDate = table.Column(type: "TEXT", nullable: true), + Gender = table.Column(type: "TEXT", nullable: true), + Bio = table.Column(type: "TEXT", nullable: true), + Language = table.Column(type: "TEXT", nullable: true), + TimeZone = table.Column(type: "TEXT", nullable: true), + Culture = table.Column(type: "TEXT", nullable: true), + Metadata = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserProfiles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserRoles", + columns: table => new + { + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false), + AssignedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserRoles", x => new { x.Tenant, x.UserKey, x.RoleId }); + }); + + migrationBuilder.CreateTable( + name: "UAuth_SessionChains", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ChainId = table.Column(type: "TEXT", nullable: false), + RootId = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + LastSeenAt = table.Column(type: "TEXT", nullable: false), + AbsoluteExpiresAt = table.Column(type: "TEXT", nullable: true), + DeviceId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Device = table.Column(type: "TEXT", nullable: false), + ClaimsSnapshot = table.Column(type: "TEXT", nullable: false), + ActiveSessionId = table.Column(type: "TEXT", nullable: true), + RotationCount = table.Column(type: "INTEGER", nullable: false), + TouchCount = table.Column(type: "INTEGER", nullable: false), + SecurityVersionAtCreation = table.Column(type: "INTEGER", nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_SessionChains", x => x.Id); + table.UniqueConstraint("AK_UAuth_SessionChains_Tenant_ChainId", x => new { x.Tenant, x.ChainId }); + table.ForeignKey( + name: "FK_UAuth_SessionChains_UAuth_SessionRoots_Tenant_RootId", + columns: x => new { x.Tenant, x.RootId }, + principalTable: "UAuth_SessionRoots", + principalColumns: new[] { "Tenant", "RootId" }, + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UAuth_Sessions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SessionId = table.Column(type: "TEXT", nullable: false), + ChainId = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ExpiresAt = table.Column(type: "TEXT", nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + SecurityVersionAtCreation = table.Column(type: "INTEGER", nullable: false), + Device = table.Column(type: "TEXT", nullable: false), + Claims = table.Column(type: "TEXT", nullable: false), + Metadata = table.Column(type: "TEXT", nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_Sessions", x => x.Id); + table.ForeignKey( + name: "FK_UAuth_Sessions_UAuth_SessionChains_Tenant_ChainId", + columns: x => new { x.Tenant, x.ChainId }, + principalTable: "UAuth_SessionChains", + principalColumns: new[] { "Tenant", "ChainId" }, + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_LockedUntil", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "LockedUntil" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_ResetRequestedAt", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "ResetRequestedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey", "Scope" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope_CredentialType", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey", "Scope", "CredentialType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_ExpiresAt", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "ExpiresAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_Id", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_RevokedAt", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_UserKey", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_UserKey_DeletedAt", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "UserKey", "DeletedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ChainId", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ChainId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ExpiresAt", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ExpiresAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ExpiresAt_RevokedAt", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ExpiresAt", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ReplacedByTokenHash", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ReplacedByTokenHash" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_SessionId", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "SessionId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_TokenHash", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "TokenHash" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_TokenHash_RevokedAt", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "TokenHash", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_TokenId", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "TokenId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_UserKey", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RolePermissions_Tenant_Permission", + table: "UAuth_RolePermissions", + columns: new[] { "Tenant", "Permission" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RolePermissions_Tenant_RoleId", + table: "UAuth_RolePermissions", + columns: new[] { "Tenant", "RoleId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Roles_Tenant_Id", + table: "UAuth_Roles", + columns: new[] { "Tenant", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Roles_Tenant_NormalizedName", + table: "UAuth_Roles", + columns: new[] { "Tenant", "NormalizedName" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_ChainId", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "ChainId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_RootId", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "RootId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_UserKey", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_UserKey_DeviceId", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "UserKey", "DeviceId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionRoots_Tenant_RootId", + table: "UAuth_SessionRoots", + columns: new[] { "Tenant", "RootId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionRoots_Tenant_UserKey", + table: "UAuth_SessionRoots", + columns: new[] { "Tenant", "UserKey" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_ChainId", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "ChainId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_ChainId_RevokedAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "ChainId", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_ExpiresAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "ExpiresAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_RevokedAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_SessionId", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "SessionId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_UserKey_RevokedAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "UserKey", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_NormalizedValue", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "NormalizedValue" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_Type_NormalizedValue", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "Type", "NormalizedValue" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_UserKey", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_UserKey_IsPrimary", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "UserKey", "IsPrimary" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_UserKey_Type_IsPrimary", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "UserKey", "Type", "IsPrimary" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserLifecycles_Tenant_UserKey", + table: "UAuth_UserLifecycles", + columns: new[] { "Tenant", "UserKey" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserProfiles_Tenant_UserKey_ProfileKey", + table: "UAuth_UserProfiles", + columns: new[] { "Tenant", "UserKey", "ProfileKey" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserRoles_Tenant_RoleId", + table: "UAuth_UserRoles", + columns: new[] { "Tenant", "RoleId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserRoles_Tenant_UserKey", + table: "UAuth_UserRoles", + columns: new[] { "Tenant", "UserKey" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UAuth_Authentication"); + + migrationBuilder.DropTable( + name: "UAuth_PasswordCredentials"); + + migrationBuilder.DropTable( + name: "UAuth_RefreshTokens"); + + migrationBuilder.DropTable( + name: "UAuth_RolePermissions"); + + migrationBuilder.DropTable( + name: "UAuth_Roles"); + + migrationBuilder.DropTable( + name: "UAuth_Sessions"); + + migrationBuilder.DropTable( + name: "UAuth_UserIdentifiers"); + + migrationBuilder.DropTable( + name: "UAuth_UserLifecycles"); + + migrationBuilder.DropTable( + name: "UAuth_UserProfiles"); + + migrationBuilder.DropTable( + name: "UAuth_UserRoles"); + + migrationBuilder.DropTable( + name: "UAuth_SessionChains"); + + migrationBuilder.DropTable( + name: "UAuth_SessionRoots"); + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs new file mode 100644 index 00000000..4000ca4d --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs @@ -0,0 +1,713 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + partial class UAuthDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs new file mode 100644 index 00000000..125730ca --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Program.cs @@ -0,0 +1,93 @@ +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Sample.Seed.Extensions; +using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore; +using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components; +using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Infrastructure; +using CodeBeam.UltimateAuth.Server.Extensions; +using Microsoft.EntityFrameworkCore; +using MudBlazor.Services; +using MudExtensions.Services; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddCircuitOptions(options => + { + options.DetailedErrors = true; + }); + +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); +builder.Services.AddMudExtensions(); + +builder.Services.AddScoped(); + +builder.Services.AddUltimateAuthServer(o => { + o.Diagnostics.EnableRefreshDetails = true; + //o.Session.MaxLifetime = TimeSpan.FromSeconds(32); + //o.Session.Lifetime = TimeSpan.FromSeconds(32); + //o.Session.TouchInterval = TimeSpan.FromSeconds(9); + //o.Session.IdleTimeout = TimeSpan.FromSeconds(15); + //o.Token.AccessTokenLifetime = TimeSpan.FromSeconds(30); + //o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32); + o.Login.MaxFailedAttempts = 2; + o.Login.LockoutDuration = TimeSpan.FromSeconds(10); + o.Identifiers.AllowMultipleUsernames = true; + o.UserProfile.EnableMultiProfile = true; +}) + .AddUltimateAuthEntityFrameworkCore(db => + { + db.UseSqlite("Data Source=uauthhub.db", x => x.MigrationsAssembly("CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore")); + }) + .AddUAuthHub(o => o.AllowedClientOrigins.Add("https://localhost:6132")); // Client sample's URL + +builder.Services.AddScopedUltimateAuthSampleSeed(); + +builder.Services.AddUltimateAuthClientBlazor(o => +{ + //o.Refresh.Interval = TimeSpan.FromSeconds(5); + o.Reauth.Behavior = ReauthBehavior.RaiseEvent; +}); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} +else +{ + app.MapOpenApi(); + app.MapScalarApiReference(); + + using (var scope = app.Services.CreateScope()) + { + await UAuthDbInitializer.InitializeAsync(app.Services, reset: true); + + var seedRunner = scope.ServiceProvider.GetRequiredService(); + await seedRunner.RunAsync(null); + } +} + +app.UseHttpsRedirection(); + +app.UseUltimateAuthWithAspNetCore(); +app.UseAntiforgery(); + +app.MapUltimateAuthEndpoints(); +app.MapUAuthHub(); +app.MapStaticAssets(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient()); + +app.Run(); diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Properties/launchSettings.json b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Properties/launchSettings.json new file mode 100644 index 00000000..591418ff --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:6113", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:6112;http://localhost:6113", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Seed/UAuthDbInitializer.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Seed/UAuthDbInitializer.cs new file mode 100644 index 00000000..85f801a9 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Seed/UAuthDbInitializer.cs @@ -0,0 +1,46 @@ +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore; + +public static class UAuthDbInitializer +{ + public static async Task InitializeAsync(IServiceProvider services, bool reset = false) + { + using var scope = services.CreateScope(); + var sp = scope.ServiceProvider; + + var bundleDb = sp.GetService(); + + if (bundleDb != null) + { + if (reset) + await bundleDb.Database.EnsureDeletedAsync(); + + await bundleDb.Database.MigrateAsync(); + return; + } + + var contexts = new DbContext[] + { + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService() + }; + + if (reset) + await contexts[0].Database.EnsureDeletedAsync(); + + foreach (var db in contexts) + await db.Database.MigrateAsync(); + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.Development.json b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.json b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db new file mode 100644 index 0000000000000000000000000000000000000000..4e86411b5803e34b1e4767ce981907694f15c1ed GIT binary patch literal 4096 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 - - net10.0 - enable - enable - 0.0.1 - false - true - + + net10.0 + enable + enable + false + true + - - - - - - + + + + + + - - - - - - - + + + + + + +
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor new file mode 100644 index 00000000..2806b7d3 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor @@ -0,0 +1,19 @@ +@namespace CodeBeam.UltimateAuth.Sample +@inherits ComponentBase + + + + @if (Variant == UAuthLogoVariant.Brand) + { + + + + } + else + { + + + + } + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor.cs new file mode 100644 index 00000000..030d9b66 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogo.razor.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace CodeBeam.UltimateAuth.Sample; + +public partial class UAuthLogo : ComponentBase +{ + [Parameter] public UAuthLogoVariant Variant { get; set; } = UAuthLogoVariant.Brand; + + [Parameter] public int Size { get; set; } = 32; + + [Parameter] public string? ShieldColor { get; set; } = "#00072d"; + [Parameter] public string? KeyColor { get; set; } = "#f6f5ae"; + + [Parameter] public string? Class { get; set; } + [Parameter] public string? Style { get; set; } + + private string BuildStyle() + { + if (Variant == UAuthLogoVariant.Mono) + return $"color: {KeyColor}; {Style}"; + + return Style ?? ""; + } + + protected string KeyPath => @" +M120.43,39.44H79.57A11.67,11.67,0,0,0,67.9,51.11V77.37 +A11.67,11.67,0,0,0,79.57,89H90.51l3.89,3.9v5.32l-3.8,3.81v81.41H99 +v-5.33h13.69V169H108.1v-3.8H99C99,150.76,111.9,153,111.9,153 +V99.79h-8V93.32L108.19,89h12.24 +A11.67,11.67,0,0,0,132.1,77.37V51.11 +A11.67,11.67,0,0,0,120.43,39.44Z + +M79.57,48.19h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.84a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.84a2.91,2.91 0 0 1 2.91,-2.92Z + +M79.57,68.62h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.83a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.83a2.91,2.91 0 0 1 2.91,-2.92Z + +M114.59,48.19h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.84a2.91,2.91 0 0 1 -2.91,2.91 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.91 +v-5.84a2.92,2.92 0 0 1 2.92,-2.92Z + +M114.59,68.62h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.83a2.91,2.91 0 0 1 -2.91,2.92 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.92 +v-5.83a2.92,2.92 0 0 1 2.92,-2.92Z +"; +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogoVariant.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogoVariant.cs new file mode 100644 index 00000000..fe3be220 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Brand/UAuthLogoVariant.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample; + +public enum UAuthLogoVariant +{ + Brand, + Mono +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj new file mode 100644 index 00000000..93156414 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + true + Default + false + true + + + + + + + + + + + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Common/UAuthDialog.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Common/UAuthDialog.cs new file mode 100644 index 00000000..caa73d6b --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Common/UAuthDialog.cs @@ -0,0 +1,29 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Common; + +public static class UAuthDialog +{ + public static DialogParameters GetDialogParameters(UAuthState state, UserKey? userKey = null) + { + DialogParameters parameters = new DialogParameters(); + parameters.Add("AuthState", state); + if (userKey != null ) + { + parameters.Add("UserKey", userKey); + } + return parameters; + } + + public static DialogOptions GetDialogOptions(MaxWidth maxWidth = MaxWidth.Medium) + { + return new DialogOptions + { + MaxWidth = maxWidth, + FullWidth = true, + CloseButton = true + }; + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Custom/UAuthPageComponent.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Custom/UAuthPageComponent.razor new file mode 100644 index 00000000..5af543e4 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Custom/UAuthPageComponent.razor @@ -0,0 +1,10 @@ + + + @ChildContent + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor new file mode 100644 index 00000000..0c91e45c --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor @@ -0,0 +1,23 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + Identifier Management + User: @AuthState?.Identity?.DisplayName + + + + + Suspend Account + + + + Delete Account + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor.cs new file mode 100644 index 00000000..d5652e62 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/AccountStatusDialog.razor.cs @@ -0,0 +1,77 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class AccountStatusDialog +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + private async Task SuspendAccountAsync() + { + var info = await DialogService.ShowMessageBoxAsync( + title: "Are You Sure", + markupMessage: (MarkupString) + """ + You are going to suspend your account.

+ You can still active your account later. + """, + yesText: "Suspend", noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (info != true) + { + Snackbar.Add("Suspend process cancelled.", Severity.Info); + return; + } + + ChangeUserStatusSelfRequest request = new() { NewStatus = SelfAssignableUserStatus.SelfSuspended }; + var result = await UAuthClient.Users.ChangeMyStatusAsync(request); + if (result.IsSuccess) + { + Snackbar.Add("Your account suspended successfully.", Severity.Success); + MudDialog.Close(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Delete failed.", Severity.Error); + } + } + + private async Task DeleteAccountAsync() + { + var info = await DialogService.ShowMessageBoxAsync( + title: "Are You Sure", + markupMessage: (MarkupString) + """ + You are going to delete your account.

+ This action can't be undone.

+ (Actually it is, admin can handle soft deleted accounts.) + """, + yesText: "Delete", noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (info != true) + { + Snackbar.Add("Deletion cancelled.", Severity.Info); + return; + } + + var result = await UAuthClient.Users.DeleteMeAsync(); + if (result.IsSuccess) + { + Snackbar.Add("Your account deleted successfully.", Severity.Success); + MudDialog.Close(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Delete failed.", Severity.Error); + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor new file mode 100644 index 00000000..9a514935 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor @@ -0,0 +1,27 @@ +@using CodeBeam.UltimateAuth.Credentials.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar + + + + Create User + + + + + + + + + + + + + + + + Cancel + Create + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor.cs new file mode 100644 index 00000000..d96ee282 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CreateUserDialog.razor.cs @@ -0,0 +1,55 @@ +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class CreateUserDialog +{ + private MudForm _form = null!; + private string? _username; + private string? _email; + private string? _password; + private string? _passwordCheck; + private string? _displayName; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + private async Task CreateUserAsync() + { + await _form.ValidateAsync(); + + if (!_form.IsValid) + return; + + if (_password != _passwordCheck) + { + Snackbar.Add("Passwords don't match.", Severity.Error); + return; + } + + var request = new CreateUserRequest + { + UserName = _username, + Email = _email, + DisplayName = _displayName, + Password = _password + }; + + var result = await UAuthClient.Users.CreateAsAdminAsync(request); + + if (!result.IsSuccess) + { + Snackbar.Add(result.ErrorText ?? "User creation failed.", Severity.Error); + return; + } + + Snackbar.Add("User created successfully", Severity.Success); + MudDialog.Close(DialogResult.Ok(true)); + } + + private string PasswordMatch(string? arg) => _password != arg ? "Passwords don't match." : string.Empty; + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor new file mode 100644 index 00000000..660b7c3a --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor @@ -0,0 +1,51 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Credentials.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IUAuthStateManager StateManager +@inject NavigationManager Nav + + + + Credential Management + User: @AuthState?.Identity?.DisplayName + + + + + @if (UserKey == null) + { + + + + } + else + { + + + Administrators can directly assign passwords to users. + However, using the credential reset flow is generally recommended for better security and auditability. + + + } + + + + + + + + + + + @(UserKey is null ? "Change Password" : "Set Password") + + + + + + Cancel + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor.cs new file mode 100644 index 00000000..93228c65 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/CredentialDialog.razor.cs @@ -0,0 +1,92 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Credentials.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class CredentialDialog +{ + private MudForm _form = null!; + private string? _oldPassword; + private string? _newPassword; + private string? _newPasswordCheck; + private bool _passwordMode1 = false; + private bool _passwordMode2 = false; + private bool _passwordMode3 = true; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + private async Task ChangePasswordAsync() + { + if (_form is null) + return; + + await _form.ValidateAsync(); + if (!_form.IsValid) + { + Snackbar.Add("Form is not valid.", Severity.Error); + return; + } + + if (_newPassword != _newPasswordCheck) + { + Snackbar.Add("New password and check do not match", Severity.Error); + return; + } + + ChangeCredentialRequest request; + + if (UserKey is null) + { + request = new ChangeCredentialRequest + { + CurrentSecret = _oldPassword!, + NewSecret = _newPassword! + }; + } + else + { + request = new ChangeCredentialRequest + { + NewSecret = _newPassword! + }; + } + + UAuthResult result; + if (UserKey is null) + { + result = await UAuthClient.Credentials.ChangeMyAsync(request); + } + else + { + result = await UAuthClient.Credentials.ChangeUserAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Password changed successfully", Severity.Success); + _oldPassword = null; + _newPassword = null; + _newPasswordCheck = null; + MudDialog.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add(result.ErrorText ?? "An error occurred while changing password", Severity.Error); + } + } + + private string PasswordMatch(string arg) => _newPassword != arg ? "Passwords don't match" : string.Empty; + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor new file mode 100644 index 00000000..24c9e8c9 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor @@ -0,0 +1,115 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + Identifier Management + + @if (UserKey is null) + { + User: @AuthState?.Identity?.DisplayName + } + else + { + UserKey: @UserKey.Value + } + + + + @if (_loaded) + { + + + + Identifiers + + + + + + + + + + + + + + + + + + + + + + + + + @if (context.Item.IsPrimary) + { + + + + } + else + { + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add + + + + + } + else + { +
+ +
+ } +
+ + + Cancel + +
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor.cs new file mode 100644 index 00000000..135cdc64 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/IdentifierDialog.razor.cs @@ -0,0 +1,311 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class IdentifierDialog +{ + private MudDataGrid? _grid; + private UserIdentifierType _newIdentifierType; + private string? _newIdentifierValue; + private bool _newIdentifierPrimary; + private bool _loading = false; + private bool _reloadQueued; + private bool _loaded; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + _loaded = true; + var result = await UAuthClient.Identifiers.GetMyAsync(); + if (result != null && result.IsSuccess && result.Value != null) + { + await ReloadAsync(); + } + StateHasChanged(); + } + } + + private async Task> LoadServerData(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new PageRequest + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + UAuthResult> res; + + if (UserKey is null) + { + res = await UAuthClient.Identifiers.GetMyAsync(req); + } + else + { + res = await UAuthClient.Identifiers.GetUserAsync(UserKey.Value, req); + } + + if (!res.IsSuccess || res.Value is null) + { + Snackbar.Add(res.Problem?.Title ?? "Failed", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task ReloadAsync() + { + if (_loading) + { + _reloadQueued = true; + return; + } + + if (_grid is null) + return; + + _loading = true; + await InvokeAsync(StateHasChanged); + await Task.Delay(300); + + try + { + await _grid.ReloadServerData(); + } + finally + { + _loading = false; + + if (_reloadQueued) + { + _reloadQueued = false; + await ReloadAsync(); + } + + await InvokeAsync(StateHasChanged); + } + } + + private async Task CommittedItemChanges(UserIdentifierInfo item) + { + UpdateUserIdentifierRequest updateRequest = new() + { + Id = item.Id, + NewValue = item.Value + }; + + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.UpdateMyAsync(updateRequest); + } + else + { + result = await UAuthClient.Identifiers.UpdateUserAsync(UserKey.Value, updateRequest); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier updated successfully", Severity.Success); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to update identifier", Severity.Error); + } + + await ReloadAsync(); + return DataGridEditFormAction.Close; + } + + private async Task AddNewIdentifier() + { + if (string.IsNullOrEmpty(_newIdentifierValue)) + { + Snackbar.Add("Value cannot be empty", Severity.Warning); + return; + } + + AddUserIdentifierRequest request = new() + { + Type = _newIdentifierType, + Value = _newIdentifierValue, + IsPrimary = _newIdentifierPrimary + }; + + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.AddMyAsync(request); + } + else + { + result = await UAuthClient.Identifiers.AddUserAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier added successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to add identifier", Severity.Error); + } + } + + private async Task VerifyAsync(Guid id) + { + var demoInfo = await DialogService.ShowMessageBoxAsync( + title: "Demo verification", + markupMessage: (MarkupString) + """ + This is a demo action.

+ In a real app, you should verify identifiers via Email, SMS, or an Authenticator flow. + This will only mark the identifier as verified in UltimateAuth. + """, + yesText: "Verify", noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (demoInfo != true) + { + Snackbar.Add("Verification cancelled", Severity.Info); + return; + } + + VerifyUserIdentifierRequest request = new() { Id = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.VerifyMyAsync(request); + } + else + { + result = await UAuthClient.Identifiers.VerifyUserAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier verified successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to verify primary identifier", Severity.Error); + } + } + + private async Task SetPrimaryAsync(Guid id) + { + SetPrimaryUserIdentifierRequest request = new() { Id = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.SetMyPrimaryAsync(request); + } + else + { + result = await UAuthClient.Identifiers.SetUserPrimaryAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier set successfully.", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to set primary identifier", Severity.Error); + } + } + + private async Task UnsetPrimaryAsync(Guid id) + { + UnsetPrimaryUserIdentifierRequest request = new() { Id = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.UnsetMyPrimaryAsync(request); + } + else + { + result = await UAuthClient.Identifiers.UnsetUserPrimaryAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier unset successfully.", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to unset primary identifier", Severity.Error); + } + } + + private async Task DeleteIdentifier(Guid id) + { + DeleteUserIdentifierRequest request = new() { Id = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.DeleteMyAsync(request); + } + else + { + result = await UAuthClient.Identifiers.DeleteUserAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier deleted successfully.", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to delete identifier", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor new file mode 100644 index 00000000..8e0df863 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor @@ -0,0 +1,46 @@ +@using CodeBeam.UltimateAuth.Authorization.Contracts +@using CodeBeam.UltimateAuth.Core.Defaults +@using System.Reflection + +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar + + + + Role Permissions + @Role.Name + + + + @* For Debug *@ + @* Current Permissions: @string.Join(", ", Role.Permissions) *@ + + @foreach (var group in _groups) + { + + + + + @group.Name (@group.Items.Count(x => x.Selected)/@group.Items.Count) + + + + + @foreach (var perm in group.Items) + { + + + + } + + + + } + + + + + Cancel + Save + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor.cs new file mode 100644 index 00000000..b6b03bb8 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/PermissionDialog.razor.cs @@ -0,0 +1,120 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class PermissionDialog +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public RoleInfo Role { get; set; } = default!; + + private List _groups = new(); + + protected override void OnInitialized() + { + var catalog = UAuthPermissionCatalog.GetAdminPermissions(); + var expanded = PermissionExpander.Expand(Role.Permissions, catalog); + var selected = expanded.Select(x => x.Value).ToHashSet(); + + _groups = catalog + .GroupBy(p => p.Split('.')[0]) + .Select(g => new PermissionGroup + { + Name = g.Key, + Items = g.Select(p => new PermissionItem + { + Value = p, + Selected = selected.Contains(p) + }).ToList() + }) + .OrderBy(x => x.Name) + .ToList(); + } + + private void ToggleGroup(PermissionGroup group, bool value) + { + foreach (var item in group.Items) + item.Selected = value; + } + + private void TogglePermission(PermissionItem item, bool value) + { + item.Selected = value; + } + + private bool? GetGroupState(PermissionGroup group) + { + var selected = group.Items.Count(x => x.Selected); + + if (selected == 0) + return false; + + if (selected == group.Items.Count) + return true; + + return null; + } + + private async Task Save() + { + var permissions = _groups.SelectMany(g => g.Items).Where(x => x.Selected).Select(x => Permission.From(x.Value)).ToList(); + + var req = new SetRolePermissionsRequest + { + RoleId = Role.Id, + Permissions = permissions + }; + + var result = await UAuthClient.Authorization.SetRolePermissionsAsync(req); + + if (!result.IsSuccess) + { + Snackbar.Add(result.ErrorText ?? "Failed to update permissions", Severity.Error); + return; + } + + var result2 = await UAuthClient.Authorization.QueryRolesAsync(new RoleQuery() { Search = Role.Name }); + if (result2.Value?.Items is not null) + { + Role = result2.Value.Items.First(); + } + + Snackbar.Add("Permissions updated", Severity.Success); + RefreshUI(); + } + + private void RefreshUI() + { + var catalog = UAuthPermissionCatalog.GetAdminPermissions(); + var expanded = PermissionExpander.Expand(Role.Permissions, catalog); + var selected = expanded.Select(x => x.Value).ToHashSet(); + + foreach (var group in _groups) + { + foreach (var item in group.Items) + { + item.Selected = selected.Contains(item.Value); + } + } + + StateHasChanged(); + } + + private void Cancel() => MudDialog.Cancel(); + + private class PermissionGroup + { + public string Name { get; set; } = ""; + public List Items { get; set; } = new(); + } + + private class PermissionItem + { + public string Value { get; set; } = ""; + public bool Selected { get; set; } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor new file mode 100644 index 00000000..a36af169 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor @@ -0,0 +1,103 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + Identifier Management + + @if (UserKey is null) + { + User: @AuthState?.Identity?.DisplayName + } + else + { + UserKey: @UserKey.Value + } + + + + @if (_loaded) + { + + + + + + Name + + + + + + + + + + + + + + + + + + + Personal + + + + + + + + + + + + + + + + + + + Localization + + + + + + + + + + + @foreach (var tz in TimeZoneInfo.GetSystemTimeZones()) + { + @tz.Id - @tz.DisplayName + } + + + + + + + + + + } + else + { +
+ +
+ } +
+ + Cancel + Save + +
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor.cs new file mode 100644 index 00000000..50a8d74a --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ProfileDialog.razor.cs @@ -0,0 +1,116 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class ProfileDialog +{ + private MudForm? _form; + private string? _firstName; + private string? _lastName; + private string? _displayName; + private DateTime? _birthDate; + private string? _gender; + private string? _bio; + private string? _language; + private string? _timeZone; + private string? _culture; + private bool _loaded; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + protected override async Task OnInitializedAsync() + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Users.GetMeAsync(); + } + else + { + result = await UAuthClient.Users.GetUserAsync(UserKey.Value); + } + + if (result.IsSuccess && result.Value is not null) + { + var p = result.Value; + + _firstName = p.FirstName; + _lastName = p.LastName; + _displayName = p.DisplayName; + + _gender = p.Gender; + _birthDate = p.BirthDate?.ToDateTime(TimeOnly.MinValue); + _bio = p.Bio; + + _language = p.Language; + _timeZone = p.TimeZone; + _culture = p.Culture; + } + _loaded = true; + } + + private async Task SaveAsync() + { + if (AuthState is null || AuthState.Identity is null) + { + Snackbar.Add("No AuthState found.", Severity.Error); + return; + } + + if (_form is not null) + { + await _form.ValidateAsync(); + if (!_form.IsValid) + return; + } + + var request = new UpdateProfileRequest + { + FirstName = _firstName, + LastName = _lastName, + DisplayName = _displayName, + BirthDate = _birthDate.HasValue ? DateOnly.FromDateTime(_birthDate.Value) : null, + Gender = _gender, + Bio = _bio, + Language = _language, + TimeZone = _timeZone, + Culture = _culture + }; + + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Users.UpdateMeAsync(request); + } + else + { + result = await UAuthClient.Users.UpdateUserAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Profile updated", Severity.Success); + MudDialog.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to update profile", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor new file mode 100644 index 00000000..06a515aa --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor @@ -0,0 +1,38 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Credentials.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IUAuthStateManager StateManager +@inject NavigationManager Nav + + + + Reset Credential + + + + + + This is a demonstration of how to implement a credential reset flow. + In a production application, you should use reset token or code in email, SMS etc. verification steps. + + + Reset request always returns ok even with not found users due to security reasons. + + + Request Reset + @if (_resetRequested) + { + Your reset code is: (Copy it before next step) + @_resetCode + Use Reset Code + } + + + + + Close + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor.cs new file mode 100644 index 00000000..04657723 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResetDialog.razor.cs @@ -0,0 +1,42 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Credentials.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class ResetDialog +{ + private bool _resetRequested = false; + private string? _resetCode; + private string? _identifier; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + private async Task RequestResetAsync() + { + var request = new BeginResetCredentialRequest + { + CredentialType = CredentialType.Password, + ResetCodeType = ResetCodeType.Code, + Identifier = _identifier ?? string.Empty + }; + + var result = await UAuthClient.Credentials.BeginResetMyAsync(request); + if (!result.IsSuccess || result.Value is null) + { + Snackbar.Add(result.ErrorText ?? "Failed to request credential reset.", Severity.Error); + return; + } + + _resetCode = result.Value.Token; + _resetRequested = true; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor new file mode 100644 index 00000000..c235fb4b --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor @@ -0,0 +1,54 @@ +@using CodeBeam.UAuth.Sample.IntWasm.Client.ResourceApi +@inject ProductApiService Api +@inject ISnackbar Snackbar + + + + + Resource Api + Sample demonstration of a resource. + + + + Reload + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add + + + + + + + \ No newline at end of file diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor.cs new file mode 100644 index 00000000..7b6f3ebb --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/ResourceApiDialog.razor.cs @@ -0,0 +1,95 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UAuth.Sample.IntWasm.Client.ResourceApi; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class ResourceApiDialog +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + private List _products = new List(); + private string? _newName = null; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _products = (await Api.GetAllAsync()).Value ?? new(); + StateHasChanged(); + } + } + + private async Task CommittedItemChanges(SampleProduct item) + { + var result = await Api.UpdateAsync(item.Id, item); + + if (result.IsSuccess) + { + Snackbar.Add("Product updated successfully", Severity.Success); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to update product.", Severity.Error); + } + + return DataGridEditFormAction.Close; + } + + private async Task GetProducts() + { + var result = await Api.GetAllAsync(); + + if (result.IsSuccess) + { + _products = result.Value ?? new(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Process failed.", Severity.Error); + } + } + + private async Task CreateProduct() + { + var product = new SampleProduct + { + Name = _newName + }; + + var result = await Api.CreateAsync(product); + + if (result.IsSuccess) + { + Snackbar.Add("New product created."); + _products = (await Api.GetAllAsync()).Value ?? new(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Process failed.", Severity.Error); + } + } + + private async Task DeleteProduct(int id) + { + var result = await Api.DeleteAsync(id); + + if (result.IsSuccess) + { + Snackbar.Add("Product deleted succesfully.", Severity.Success); + _products = (await Api.GetAllAsync()).Value ?? new(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Process failed.", Severity.Error); + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor new file mode 100644 index 00000000..bfcf9428 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor @@ -0,0 +1,90 @@ +@using CodeBeam.UltimateAuth.Authorization.Contracts +@using CodeBeam.UltimateAuth.Core.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + + Role Management + Manage system roles + + + + @if (_loaded) + { + + + + + Roles + + + + + + + + + + + @GetPermissionCount(context.Item) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + + + + + } + else + { +
+ +
+ } +
+ + + Close + +
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor.cs new file mode 100644 index 00000000..f079d51a --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/RoleDialog.razor.cs @@ -0,0 +1,176 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class RoleDialog +{ + private MudDataGrid? _grid; + private bool _loading; + private string? _newRoleName; + private bool _loaded; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + _loaded = true; + StateHasChanged(); + } + } + + private async Task> LoadServerData(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new RoleQuery + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + var res = await UAuthClient.Authorization.QueryRolesAsync(req); + + if (!res.IsSuccess || res.Value == null) + { + Snackbar.Add(res.ErrorText ?? "Failed", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task CommittedItemChanges(RoleInfo role) + { + var req = new RenameRoleRequest + { + Id = role.Id, + Name = role.Name + }; + + var result = await UAuthClient.Authorization.RenameRoleAsync(req); + + if (result.IsSuccess) + { + Snackbar.Add("Role renamed", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Rename failed", Severity.Error); + } + + await ReloadAsync(); + return DataGridEditFormAction.Close; + } + + private async Task CreateRole() + { + if (string.IsNullOrWhiteSpace(_newRoleName)) + { + Snackbar.Add("Role name required.", Severity.Warning); + return; + } + + var req = new CreateRoleRequest + { + Name = _newRoleName + }; + + var res = await UAuthClient.Authorization.CreateRoleAsync(req); + + if (res.IsSuccess) + { + Snackbar.Add("Role created.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(res.ErrorText ?? "Creation failed.", Severity.Error); + } + } + + private async Task DeleteRole(RoleId roleId) + { + var confirm = await DialogService.ShowMessageBoxAsync( + "Delete role", + "Are you sure?", + yesText: "Delete", + cancelText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (confirm != true) + return; + + var req = new DeleteRoleRequest() { Id = roleId }; + var result = await UAuthClient.Authorization.DeleteRoleAsync(req); + + if (result.IsSuccess) + { + Snackbar.Add($"Role deleted, assignments removed from {result.Value?.RemovedAssignments.ToString() ?? "unknown"} users.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Deletion failed.", Severity.Error); + } + } + + private async Task EditPermissions(RoleInfo role) + { + var dialog = await DialogService.ShowAsync( + "Edit Permissions", + new DialogParameters + { + { nameof(PermissionDialog.Role), role } + }, + new DialogOptions + { + CloseButton = true, + MaxWidth = MaxWidth.Large, + FullWidth = true + }); + + var result = await dialog.Result; + await ReloadAsync(); + } + + private async Task ReloadAsync() + { + _loading = true; + await Task.Delay(300); + if (_grid is null) + return; + + await _grid.ReloadServerData(); + _loading = false; + } + + private int GetPermissionCount(RoleInfo role) + { + var expanded = PermissionExpander.Expand(role.Permissions, UAuthPermissionCatalog.GetAdminPermissions()); + return expanded.Count; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor new file mode 100644 index 00000000..e5ee0c4d --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor @@ -0,0 +1,226 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IUAuthStateManager StateManager +@inject NavigationManager Nav + + + + Session Management + + @if (UserKey is null) + { + User: @AuthState?.Identity?.DisplayName + } + else + { + UserKey: @UserKey.Value + } + + + + @if (_loaded) + { + @if (_chainDetail is not null) + { + + + + Device Details + + + + @if (!_chainDetail.IsRevoked) + { + + Revoke Device + + } + + + + + + + Device Type + @_chainDetail.DeviceType + + + + Platform + @_chainDetail.Platform + + + + Operating System + @_chainDetail.OperatingSystem + + + + Browser + @_chainDetail.Browser + + + + Created + @_chainDetail.CreatedAt.ToLocalTime() + + + + Last Seen + @_chainDetail.LastSeenAt.ToLocalTime() + + + + State + + @_chainDetail.State + + + + + Active Session + @_chainDetail.ActiveSessionId + + + + Rotation Count + @_chainDetail.RotationCount + + + + Touch Count + @_chainDetail.TouchCount + + + + + + Session History + + + + Session Id + Created + Expires + Status + + + + @context.SessionId + @context.CreatedAt.ToLocalTime() + @context.ExpiresAt.ToLocalTime() + + @if (context.IsRevoked) + { + Revoked + } + else + { + Active + } + + + + + } + else + { + + Logout All Devices + @if (UserKey == null) + { + Logout Other Devices + } + Revoke All Devices + @if (UserKey == null) + { + Revoke Other Devices + } + + + + Sessions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Id + @context.Item.ChainId + + + + Created At + @context.Item.CreatedAt + + + + Touch Count + @context.Item.TouchCount + + + + Rotation Count + @context.Item.RotationCount + + + + + + + + + } + } + else + { +
+ +
+ } +
+ + Cancel + +
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor.cs new file mode 100644 index 00000000..b7b084d3 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/SessionDialog.razor.cs @@ -0,0 +1,286 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class SessionDialog +{ + private MudDataGrid? _grid; + private bool _loading = false; + private bool _reloadQueued; + private SessionChainDetail? _chainDetail; + private bool _loaded; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + _loaded = true; + var result = await UAuthClient.Sessions.GetMyChainsAsync(); + if (result != null && result.IsSuccess && result.Value != null) + { + await ReloadAsync(); + } + StateHasChanged(); + } + } + + private async Task> LoadServerData(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new PageRequest + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + UAuthResult> res; + + if (UserKey is null) + { + res = await UAuthClient.Sessions.GetMyChainsAsync(req); + } + else + { + res = await UAuthClient.Sessions.GetUserChainsAsync(UserKey.Value, req); + } + + if (!res.IsSuccess || res.Value is null) + { + Snackbar.Add(res.Problem?.Title ?? "Failed", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task ReloadAsync() + { + if (_loading) + { + _reloadQueued = true; + return; + } + + if (_grid is null) + return; + + _loading = true; + await InvokeAsync(StateHasChanged); + await Task.Delay(300); + + try + { + await _grid.ReloadServerData(); + } + finally + { + _loading = false; + + if (_reloadQueued) + { + _reloadQueued = false; + await ReloadAsync(); + } + + await InvokeAsync(StateHasChanged); + } + } + + private async Task LogoutAllAsync() + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Flows.LogoutAllMyDevicesAsync(); + } + else + { + result = await UAuthClient.Flows.LogoutAllUserDevicesAsync(UserKey.Value); + } + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of all devices.", Severity.Success); + if (UserKey is null) + Nav.NavigateTo("/login"); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task LogoutOthersAsync() + { + var result = await UAuthClient.Flows.LogoutMyOtherDevicesAsync(); + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of other devices.", Severity.Success); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout", Severity.Error); + } + } + + private async Task LogoutDeviceAsync(SessionChainId chainId) + { + LogoutDeviceRequest request = new() { ChainId = chainId }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Flows.LogoutMyDeviceAsync(request); + } + else + { + result = await UAuthClient.Flows.LogoutUserDeviceAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of device.", Severity.Success); + if (result?.Value?.CurrentChain == true) + { + Nav.NavigateTo("/login"); + return; + } + await ReloadAsync(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task RevokeAllAsync() + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Sessions.RevokeAllMyChainsAsync(); + } + else + { + result = await UAuthClient.Sessions.RevokeAllUserChainsAsync(UserKey.Value); + } + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of all devices.", Severity.Success); + + if (UserKey is null) + Nav.NavigateTo("/login"); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task RevokeOthersAsync() + { + var result = await UAuthClient.Sessions.RevokeMyOtherChainsAsync(); + if (result.IsSuccess) + { + Snackbar.Add("Revoked all other devices.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task RevokeChainAsync(SessionChainId chainId) + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Sessions.RevokeMyChainAsync(chainId); + } + else + { + result = await UAuthClient.Sessions.RevokeUserChainAsync(UserKey.Value, chainId); + } + + if (result.IsSuccess) + { + Snackbar.Add("Device revoked successfully.", Severity.Success); + + if (result?.Value?.CurrentChain == true) + { + Nav.NavigateTo("/login"); + return; + } + await ReloadAsync(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task ShowChainDetailsAsync(SessionChainId chainId) + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Sessions.GetMyChainDetailAsync(chainId); + } + else + { + result = await UAuthClient.Sessions.GetUserChainDetailAsync(UserKey.Value, chainId); + } + + if (result.IsSuccess) + { + _chainDetail = result.Value; + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to fetch chain details.", Severity.Error); + _chainDetail = null; + } + } + + private void ClearDetail() + { + _chainDetail = null; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor new file mode 100644 index 00000000..39053d5e --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor @@ -0,0 +1,75 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UAuth.Sample.IntWasm.Client.Common +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + + User Management + @_user?.UserKey.Value + + + + + + + + Display Name + @_user?.DisplayName + + + + Username + @_user?.UserName + + + + Email + @_user?.PrimaryEmail + + + + Phone + @_user?.PrimaryPhone + + + + Created + @_user?.CreatedAt?.ToLocalTime() + + + + Status + @_user?.Status + + + @foreach (var s in Enum.GetValues()) + { + @s + } + + Change + + + + + + + + Management + + Sessions + Profile + Identifiers + Credentials + Roles + + + + + + Close + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor.cs new file mode 100644 index 00000000..842af288 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserDetailDialog.razor.cs @@ -0,0 +1,100 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UAuth.Sample.IntWasm.Client.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class UserDetailDialog +{ + private UserView? _user; + private AdminAssignableUserStatus _status; + + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey UserKey { get; set; } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + var result = await UAuthClient.Users.GetUserAsync(UserKey); + + if (result.IsSuccess) + { + _user = result.Value; + _status = _user?.Status.ToAdminAssignableUserStatus() ?? AdminAssignableUserStatus.Unknown; + } + } + + private async Task OpenSessions() + { + await DialogService.ShowAsync("Session Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenProfile() + { + await DialogService.ShowAsync("Profile Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenIdentifiers() + { + await DialogService.ShowAsync("Identifier Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenCredentials() + { + await DialogService.ShowAsync("Credentials", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenRoles() + { + await DialogService.ShowAsync("Roles", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task ChangeStatusAsync() + { + if (_user is null) + return; + + ChangeUserStatusAdminRequest request = new() + { + NewStatus = _status, + }; + + var result = await UAuthClient.Users.ChangeUserStatusAsync(_user.UserKey, request); + + if (result.IsSuccess) + { + Snackbar.Add("User status updated", Severity.Success); + _user = _user with { Status = _status.ToUserStatus() }; + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed", Severity.Error); + } + } + + private Color GetStatusColor(UserStatus? status) + { + return status switch + { + UserStatus.Active => Color.Success, + UserStatus.Suspended => Color.Warning, + UserStatus.Disabled => Color.Error, + _ => Color.Default + }; + } + + private void Close() + { + MudDialog.Close(); + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor new file mode 100644 index 00000000..6e754848 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor @@ -0,0 +1,49 @@ +@using CodeBeam.UltimateAuth.Authorization.Contracts +@inject IUAuthClient UAuthClient +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + + + User Roles + UserKey: @UserKey.Value + + + + + Assigned Roles + + @if (_roles.Count == 0) + { + No roles assigned + } + + + @foreach (var role in _roles) + { + @role + } + + + + + Add Role + + + + @foreach (var role in _allRoles) + { + @role.Name + } + + + Add + + + + + + Close + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor.cs new file mode 100644 index 00000000..b167d6db --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UserRoleDialog.razor.cs @@ -0,0 +1,124 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class UserRoleDialog +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey UserKey { get; set; } = default!; + + private List _roles = new(); + private List _allRoles = new(); + + private string? _selectedRole; + + protected override async Task OnInitializedAsync() + { + await LoadRoles(); + } + + private async Task LoadRoles() + { + var userRoles = await UAuthClient.Authorization.GetUserRolesAsync(UserKey); + + if (userRoles.IsSuccess && userRoles.Value != null) + _roles = userRoles.Value.Roles.Items.Select(x => x.Name).ToList(); + + var roles = await UAuthClient.Authorization.QueryRolesAsync(new RoleQuery + { + PageNumber = 1, + PageSize = 200 + }); + + if (roles.IsSuccess && roles.Value != null) + _allRoles = roles.Value.Items.ToList(); + } + + private async Task AddRole() + { + if (string.IsNullOrWhiteSpace(_selectedRole)) + return; + + var request = new AssignRoleRequest + { + UserKey = UserKey, + RoleName = _selectedRole + }; + + var result = await UAuthClient.Authorization.AssignRoleToUserAsync(request); + + if (result.IsSuccess) + { + _roles.Add(_selectedRole); + Snackbar.Add("Role assigned", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed", Severity.Error); + } + + _selectedRole = null; + } + + private async Task RemoveRole(string role) + { + var confirm = await DialogService.ShowMessageBoxAsync( + "Remove Role", + $"Remove {role} from user?", + yesText: "Remove", + noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (confirm != true) + { + Snackbar.Add("Role remove process cancelled.", Severity.Info); + return; + } + + if (role == "Admin") + { + var confirm2 = await DialogService.ShowMessageBoxAsync( + "Are You Sure", + "You are going to remove admin role. This action may cause the application unuseable.", + yesText: "Remove", + noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (confirm2 != true) + { + Snackbar.Add("Role remove process cancelled.", Severity.Info); + return; + } + } + + var request = new RemoveRoleRequest + { + UserKey = UserKey, + RoleName = role + }; + + var result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(request); + + if (result.IsSuccess) + { + _roles.Remove(role); + Snackbar.Add("Role removed.", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed", Severity.Error); + } + } + + private void Close() => MudDialog.Close(); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor new file mode 100644 index 00000000..0eea8438 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor @@ -0,0 +1,94 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UAuth.Sample.IntWasm.Client.Common +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + + User Management + Browse, create and manage users + + + + @if (_loaded) + { + + + + + + + + + + + + + + Users + + New User + + + + + + + + + + + @context.Item.Status + + + + + + + + + + + + + + + + + + + + + Id + @context.Item.UserKey.Value + + + + Created At + @context.Item.CreatedAt + + + + + + + + + + } + else + { +
+ +
+ } +
+ + + Close + +
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor.cs new file mode 100644 index 00000000..cc24a17b --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Components/Dialogs/UsersDialog.razor.cs @@ -0,0 +1,188 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UAuth.Sample.IntWasm.Client.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; + +public partial class UsersDialog +{ + private MudDataGrid? _grid; + private bool _loading; + private string? _search; + private bool _reloadQueued; + private UserStatus? _statusFilter; + private bool _loaded; + + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + _loaded = true; + StateHasChanged(); + } + } + + private async Task> LoadUsers(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new UserQuery + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + Search = _search, + Status = _statusFilter, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + var res = await UAuthClient.Users.QueryAsync(req); + + if (!res.IsSuccess || res.Value == null) + { + Snackbar.Add(res.ErrorText ?? "Failed to load users.", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task ReloadAsync() + { + if (_loading) + { + _reloadQueued = true; + return; + } + + if (_grid is null) + return; + + _loading = true; + await InvokeAsync(StateHasChanged); + + try + { + await _grid.ReloadServerData(); + } + finally + { + _loading = false; + + if (_reloadQueued) + { + _reloadQueued = false; + await ReloadAsync(); + } + + await InvokeAsync(StateHasChanged); + } + } + + private async Task OnStatusChanged(UserStatus? status) + { + _statusFilter = status; + await ReloadAsync(); + } + + private async Task OpenUser(UserKey userKey) + { + var dialog = await DialogService.ShowAsync("User", UAuthDialog.GetDialogParameters(AuthState, userKey), UAuthDialog.GetDialogOptions()); + await dialog.Result; + await ReloadAsync(); + } + + private async Task OpenCreateUser() + { + var dialog = await DialogService.ShowAsync( + "Create User", + new DialogOptions + { + MaxWidth = MaxWidth.Small, + FullWidth = true, + CloseButton = true + }); + + var result = await dialog.Result; + + if (result?.Canceled == false) + await ReloadAsync(); + } + + private async Task DeleteUserAsync(UserSummary user) + { + var confirm = await DialogService.ShowMessageBoxAsync( + title: "Delete user", + markupMessage: (MarkupString)$""" + Are you sure you want to delete {user.DisplayName ?? user.UserName ?? user.PrimaryEmail ?? user.UserKey}? +

+ This operation is intended for admin usage. + """, + yesText: "Delete", + cancelText: "Cancel", + options: new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + BackgroundClass = "uauth-blur-slight" + }); + + if (confirm != true) + return; + + var req = new DeleteUserRequest + { + Mode = DeleteMode.Soft + }; + + var result = await UAuthClient.Users.DeleteUserAsync(UserKey.Parse(user.UserKey, null), req); + + if (result.IsSuccess) + { + Snackbar.Add("User deleted successfully.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to delete user.", Severity.Error); + } + } + + private static Color GetStatusColor(UserStatus status) + { + return status switch + { + UserStatus.Active => Color.Success, + UserStatus.SelfSuspended => Color.Warning, + UserStatus.Suspended => Color.Warning, + UserStatus.Disabled => Color.Error, + _ => Color.Default + }; + } + + private void Close() + { + MudDialog.Close(); + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Infrastructure/DarkModeManager.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Infrastructure/DarkModeManager.cs new file mode 100644 index 00000000..3f654e18 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Infrastructure/DarkModeManager.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Infrastructure; + +public sealed class DarkModeManager +{ + private const string StorageKey = "uauth:theme:dark"; + + private readonly IClientStorage _storage; + + public DarkModeManager(IClientStorage storage) + { + _storage = storage; + } + + public async Task InitializeAsync() + { + var value = await _storage.GetAsync(StorageScope.Local, StorageKey); + + if (bool.TryParse(value, out var parsed)) + IsDarkMode = parsed; + } + + public bool IsDarkMode { get; set; } + + public event Action? Changed; + + public async Task ToggleAsync() + { + IsDarkMode = !IsDarkMode; + + await _storage.SetAsync(StorageScope.Local, StorageKey, IsDarkMode.ToString()); + Changed?.Invoke(); + } + + public void Set(bool value) + { + if (IsDarkMode == value) + return; + + IsDarkMode = value; + Changed?.Invoke(); + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor new file mode 100644 index 00000000..d1498eb6 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor @@ -0,0 +1,72 @@ +@inherits LayoutComponentBase +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject NavigationManager Nav + +@if (!RendererInfo.IsInteractive) +{ + + + +} + + + + + UltimateAuth + + Blazor WASM Sample + + + + + + + + + +
+ + + @((state.Identity?.DisplayName ?? "?").Trim() is var n ? (n.Length >= 2 ? n[..2] : n[..1]) : "?") + + +
+
+ + + @state.Identity?.DisplayName + @string.Join(", ", state.Claims.Roles) + + + + + + + + @if (state.Identity?.SessionState is not null && state.Identity.SessionState != SessionState.Active) + { + + + } + +
+
+ + + + +
+
+ + + @Body + +
+ + +
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor.cs new file mode 100644 index 00000000..dfefa793 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Layout/MainLayout.razor.cs @@ -0,0 +1,130 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UAuth.Sample.IntWasm.Client.Infrastructure; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Layout; + +public partial class MainLayout +{ + [CascadingParameter] + public UAuthState UAuth { get; set; } = default!; + + [CascadingParameter] + public DarkModeManager DarkModeManager { get; set; } = default!; + + private async Task Refresh() + { + await UAuthClient.Flows.RefreshAsync(); + } + + private async Task Logout() + { + await UAuthClient.Flows.LogoutAsync(); + } + + private Color GetBadgeColor() + { + if (UAuth is null || !UAuth.IsAuthenticated) + return Color.Error; + + if (UAuth.IsStale) + return Color.Warning; + + var state = UAuth.Identity?.SessionState; + + if (state is null || state == SessionState.Active) + return Color.Success; + + if (state == SessionState.Invalid) + return Color.Error; + + return Color.Warning; + } + + private void HandleSignInClick() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login?focus=1", replace: true, forceLoad: true); + return; + } + + GoToLoginWithReturn(); + } + + private async Task Validate() + { + try + { + var result = await UAuthClient.Flows.ValidateAsync(); + + if (result.IsValid) + { + if (result.Snapshot?.Identity.UserStatus == UserStatus.SelfSuspended) + { + Snackbar.Add("Your account is suspended by you.", Severity.Warning); + return; + } + Snackbar.Add($"Session active • Tenant: {result.Snapshot?.Identity?.Tenant.Value} • User: {result.Snapshot?.Identity?.PrimaryUserName}", Severity.Success); + } + else + { + switch (result.State) + { + case SessionState.Expired: + Snackbar.Add("Session expired. Please sign in again.", Severity.Warning); + break; + + case SessionState.DeviceMismatch: + Snackbar.Add("Session invalid for this device.", Severity.Error); + break; + + default: + Snackbar.Add($"Session state: {result.State}", Severity.Error); + break; + } + } + } + catch (UAuthTransportException) + { + Snackbar.Add("Network error.", Severity.Error); + } + catch (UAuthProtocolException) + { + Snackbar.Add("Invalid response.", Severity.Error); + } + catch (UAuthException ex) + { + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Unexpected error: {ex.Message}", Severity.Error); + } + } + + private void GoToLoginWithReturn() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login", replace: true); + return; + } + + var current = Nav.ToBaseRelativePath(uri.ToString()); + if (string.IsNullOrWhiteSpace(current)) + current = "home"; + + var returnUrl = Uri.EscapeDataString("/" + current.TrimStart('/')); + Nav.NavigateTo($"/login?returnUrl={returnUrl}", replace: true); + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AnonymousTestPage.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AnonymousTestPage.razor new file mode 100644 index 00000000..10d035ba --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AnonymousTestPage.razor @@ -0,0 +1 @@ +@page "/anonymous-test" diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor new file mode 100644 index 00000000..e5554c4e --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/AuthorizedTestPage.razor @@ -0,0 +1,26 @@ +@page "/authorized-test" +@attribute [Authorize] + + + + + + + Everything is Ok + + + If you see this section, it means you succesfully logged in. + + + + Go Profile + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + \ No newline at end of file diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor new file mode 100644 index 00000000..d1a9096c --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor @@ -0,0 +1,455 @@ +@page "/home" +@* To make Authorize attribute to work, add ResourceApi in Program.cs, but it affects performance significantly *@ +@* @attribute [Authorize] *@ +@inherits UAuthFlowPageBase + +@inject IUAuthClient UAuthClient +@inject UAuthClientDiagnostics Diagnostics +@inject AuthenticationStateProvider AuthStateProvider +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@using System.Security.Claims +@using CodeBeam.UltimateAuth.Client.Diagnostics +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Core.Defaults +@using CodeBeam.UAuth.Sample.IntWasm.Client.Components.Custom +@using Microsoft.AspNetCore.Authorization + +@if (AuthState?.Identity?.UserStatus == UserStatus.SelfSuspended) +{ + + + + Your account is suspended. Please active it before continue. + + + + Set Active + Logout + + + + return; +} + +@if (AuthState?.Identity?.UserStatus == UserStatus.Suspended) +{ + + + + Your account is suspended. Please contact with administrator. + + + + Logout + + + + return; +} + + + + + + + + + + + Session + + + + + + + Validate + + + + + + Manual Refresh + + + + + + Logout + + + + + + Account + + + + + Manage Sessions + + + + Manage Profile + + + + Manage Identifiers + + + + Manage Credentials + + + + Suspend | Delete Account + + + + Admin + + + + + + + + + @if (_showAdminPreview) + { + + Admin operations are shown for preview. Sign in as an Admin to execute them. + + } + + @if (AuthState?.IsInRole("Admin") == true || _showAdminPreview) + { + + + + @* *@ + @* *@ + User Management + @* *@ + + + + + + @* *@ + Role Management + @* *@ + + + + } + + + Resource Api + + + + + Manage Resource + + + + + + + + + + + + @((AuthState?.Identity?.DisplayName ?? "?").Substring(0, Math.Min(2, (AuthState?.Identity?.DisplayName ?? "?").Length))) + + + + @AuthState?.Identity?.DisplayName + + @foreach (var role in AuthState?.Claims?.Roles ?? Enumerable.Empty()) + { + + @role + + } + + + + + + + + + + @if (_selectedAuthState == "UAuthState") + { + + +
+ + + Tenant + + @AuthState?.Identity?.Tenant.Value +
+ +
+ + +
+ + + User Id + + @AuthState?.Identity?.UserKey.Value +
+
+ + +
+ + + Authenticated + + @(AuthState?.IsAuthenticated == true ? "Yes" : "No") +
+
+ + +
+ + + Session State + + @AuthState?.Identity?.SessionState?.ToDescriptionString() +
+
+ + +
+ + + Username + + @AuthState?.Identity?.PrimaryUserName +
+
+ + +
+ + + Display Name + + @AuthState?.Identity?.DisplayName +
+
+ + + + + + + Email + + @AuthState?.Identity?.PrimaryEmail + + + + + + Phone + + @AuthState?.Identity?.PrimaryPhone + + + + + + + + Authenticated At + + @* TODO: Add IUAuthDateTimeFormatter *@ + @FormatLocalTime(AuthState?.Identity?.AuthenticatedAt) + + + + + + Last Validated At + + @* TODO: Validation call should update last validated at *@ + @FormatLocalTime(AuthState?.LastValidatedAt) + +
+ } + else if (_selectedAuthState == "AspNetCoreState") + { + + +
+ + + Authenticated + + @(_aspNetCoreState?.Identity?.IsAuthenticated == true ? "Yes" : "No") +
+
+ + +
+ + + User Id + + @_aspNetCoreState?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value +
+
+ + +
+ + + Username + + @_aspNetCoreState?.Identity?.Name +
+
+ + +
+ + + Authentication Type + + @_aspNetCoreState?.Identity?.AuthenticationType +
+
+
+ } +
+
+
+ + + + + + @GetHealthText() + + + Lifecycle + + + + + + Started + @Diagnostics.StartCount + + @if (Diagnostics.StartedAt is not null) + { + + + + @FormatRelative(Diagnostics.StartedAt) + + + } + + + + + Stopped + @Diagnostics.StopCount + + + + + + Terminated + @Diagnostics.TerminatedCount + + @if (Diagnostics.TerminatedAt is not null) + { + + + + + @FormatRelative(Diagnostics.TerminatedAt) + + + + } + + + + + + Refresh Metrics + + + + + + + Total Attempts + @Diagnostics.RefreshAttemptCount + + + + + + + Success + + @Diagnostics.RefreshSuccessCount + + + + + + Automatic + @Diagnostics.AutomaticRefreshCount + + + + + + Manual + @Diagnostics.ManualRefreshCount + + + + + + Touched/Rotated + @Diagnostics.RefreshTouchedCount / @Diagnostics.RefreshRotatedCount + + + + + + No-Op + @Diagnostics.RefreshNoOpCount + + + + + + Reauth Required + @Diagnostics.RefreshReauthRequiredCount + + + + + + + +
+
+
diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor.cs new file mode 100644 index 00000000..c296ed00 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Home.razor.cs @@ -0,0 +1,227 @@ +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UAuth.Sample.IntWasm.Client.Common; +using CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components.Authorization; +using MudBlazor; +using System.Security.Claims; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Pages; + +public partial class Home : UAuthFlowPageBase +{ + private string _selectedAuthState = "UAuthState"; + private ClaimsPrincipal? _aspNetCoreState; + + private bool _showAdminPreview = false; + + protected override async Task OnInitializedAsync() + { + var initial = await AuthStateProvider.GetAuthenticationStateAsync(); + _aspNetCoreState = initial.User; + AuthStateProvider.AuthenticationStateChanged += OnAuthStateChanged; + Diagnostics.Changed += OnDiagnosticsChanged; + } + + private void OnAuthStateChanged(Task task) + { + _ = HandleAuthStateChangedAsync(task); + } + + private async Task HandleAuthStateChangedAsync(Task task) + { + try + { + var state = await task; + _aspNetCoreState = state.User; + await InvokeAsync(StateHasChanged); + } + catch + { + + } + } + + private void OnDiagnosticsChanged() + { + InvokeAsync(StateHasChanged); + } + + private async Task Logout() => await UAuthClient.Flows.LogoutAsync(); + + private async Task RefreshSession() => await UAuthClient.Flows.RefreshAsync(false); + + private async Task Validate() + { + try + { + var result = await UAuthClient.Flows.ValidateAsync(); + + if (result.IsValid) + { + if (result.Snapshot?.Identity.UserStatus == UserStatus.SelfSuspended) + { + Snackbar.Add("Your account is suspended by you.", Severity.Warning); + return; + } + Snackbar.Add($"Session active • Tenant: {result.Snapshot?.Identity?.Tenant.Value} • User: {result.Snapshot?.Identity?.PrimaryUserName}", Severity.Success); + } + else + { + switch (result.State) + { + case SessionState.Expired: + Snackbar.Add("Session expired. Please sign in again.", Severity.Warning); + break; + + case SessionState.DeviceMismatch: + Snackbar.Add("Session invalid for this device.", Severity.Error); + break; + + default: + Snackbar.Add($"Session state: {result.State}", Severity.Error); + break; + } + } + } + catch (UAuthTransportException) + { + Snackbar.Add("Network error.", Severity.Error); + } + catch (UAuthProtocolException) + { + Snackbar.Add("Invalid response.", Severity.Error); + } + catch (UAuthException ex) + { + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Unexpected error: {ex.Message}", Severity.Error); + } + } + + private Color GetHealthColor() + { + if (Diagnostics.RefreshReauthRequiredCount > 0) + return Color.Warning; + + if (Diagnostics.TerminatedCount > 0) + return Color.Error; + + return Color.Success; + } + + private string GetHealthText() + { + if (Diagnostics.RefreshReauthRequiredCount > 0) + return "Reauthentication Required"; + + if (Diagnostics.TerminatedCount > 0) + return "Session Terminated"; + + return "Healthy"; + } + + private string? FormatRelative(DateTimeOffset? utc) + { + if (utc is null) + return null; + + var diff = DateTimeOffset.UtcNow - utc.Value; + + if (diff.TotalSeconds < 5) + return "just now"; + + if (diff.TotalSeconds < 60) + return $"{(int)diff.Seconds} secs ago"; + + if (diff.TotalMinutes < 60) + return $"{(int)diff.TotalMinutes} min ago"; + + if (diff.TotalHours < 24) + return $"{(int)diff.TotalHours} hrs ago"; + + return utc.Value.ToLocalTime().ToString("dd MMM yyyy"); + } + + private string? FormatLocalTime(DateTimeOffset? utc) + { + return utc?.ToLocalTime().ToString("dd MMM yyyy • HH:mm:ss"); + } + + private async Task OpenProfileDialog() + { + await DialogService.ShowAsync("Manage Profile", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenIdentifierDialog() + { + await DialogService.ShowAsync("Manage Identifiers", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenSessionDialog() + { + await DialogService.ShowAsync("Manage Sessions", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenCredentialDialog() + { + await DialogService.ShowAsync("Session Diagnostics", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenAccountStatusDialog() + { + await DialogService.ShowAsync("Manage Account", GetDialogParameters(), UAuthDialog.GetDialogOptions(MaxWidth.ExtraSmall)); + } + + private async Task OpenUserDialog() + { + await DialogService.ShowAsync("User Management", GetDialogParameters(), UAuthDialog.GetDialogOptions(MaxWidth.Large)); + } + + private async Task OpenRoleDialog() + { + await DialogService.ShowAsync("Role Management", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenResourceApiDialog() + { + await DialogService.ShowAsync("Resource Api", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private DialogParameters GetDialogParameters() + { + return new DialogParameters + { + ["AuthState"] = AuthState + }; + } + + private async Task SetAccountActiveAsync() + { + ChangeUserStatusSelfRequest request = new() { NewStatus = SelfAssignableUserStatus.Active }; + var result = await UAuthClient.Users.ChangeMyStatusAsync(request); + + if (result.IsSuccess) + { + Snackbar.Add("Account activated successfully.", Severity.Success); + } + else + { + Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Activation failed.", Severity.Error); + } + } + + public override void Dispose() + { + base.Dispose(); + AuthStateProvider.AuthenticationStateChanged -= OnAuthStateChanged; + Diagnostics.Changed -= OnDiagnosticsChanged; + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor new file mode 100644 index 00000000..1e4a9016 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor @@ -0,0 +1,4 @@ +@page "/" + +@inject NavigationManager Nav +@inject AuthenticationStateProvider AuthProvider diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor.cs new file mode 100644 index 00000000..ac24e527 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/LandingPage.razor.cs @@ -0,0 +1,17 @@ +using CodeBeam.UltimateAuth.Core.Defaults; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Pages; + +public partial class LandingPage +{ + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + var state = await AuthProvider.GetAuthenticationStateAsync(); + var isAuthenticated = state.User.Identity?.IsAuthenticated == true; + + Nav.NavigateTo(isAuthenticated ? "/home" : $"{UAuthConstants.Routes.LoginRedirect}?fresh=true"); + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor new file mode 100644 index 00000000..7da73973 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor @@ -0,0 +1,135 @@ +@page "/login" +@using CodeBeam.UltimateAuth.Client.Runtime +@attribute [UAuthLoginPage] +@inherits UAuthFlowPageBase + +@implements IDisposable +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IUAuthClientProductInfoProvider ClientProductInfoProvider +@inject IDeviceIdProvider DeviceIdProvider +@inject IDialogService DialogService + + + + + + + + + + + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor.cs new file mode 100644 index 00000000..970f9128 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Login.razor.cs @@ -0,0 +1,216 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Pages; + +public partial class Login : UAuthFlowPageBase +{ + private string? _username; + private string? _password; + private UAuthClientProductInfo? _productInfo; + private MudTextField _usernameField = default!; + + private CancellationTokenSource? _lockoutCts; + private PeriodicTimer? _lockoutTimer; + private DateTimeOffset? _lockoutUntil; + private TimeSpan _remaining; + private bool _isLocked; + private DateTimeOffset? _lockoutStartedAt; + private TimeSpan _lockoutDuration; + private double _progressPercent; + private int? _remainingAttempts = null; + + protected override async Task OnInitializedAsync() + { + _productInfo = ClientProductInfoProvider.Get(); + } + + protected override Task OnUAuthPayloadAsync(AuthFlowPayload payload) + { + HandleLoginPayload(payload); + return Task.CompletedTask; + } + + protected override async Task OnFocusRequestedAsync() + { + await _usernameField.FocusAsync(); + } + + private void HandleLoginPayload(AuthFlowPayload payload) + { + if (payload.Flow != AuthFlowType.Login) + return; + + if (payload.Reason == AuthFailureReason.LockedOut && payload.LockoutUntilUtc is { } until) + { + _lockoutUntil = until; + StartCountdown(); + } + + _remainingAttempts = payload.RemainingAttempts; + + ShowLoginError(payload.Reason, payload.RemainingAttempts); + } + + private void ShowLoginError(AuthFailureReason? reason, int? remainingAttempts) + { + string message = reason switch + { + AuthFailureReason.InvalidCredentials when remainingAttempts is > 0 + => $"Invalid username or password. {remainingAttempts} attempt(s) remaining.", + + AuthFailureReason.InvalidCredentials + => "Invalid username or password.", + + AuthFailureReason.RequiresMfa + => "Multi-factor authentication required.", + + AuthFailureReason.LockedOut + => "Your account is locked.", + + _ => "Login failed." + }; + + Snackbar.Add(message, Severity.Error); + } + + private async Task StartPkceLogin() + { + string? returnUrl = null; + if (!string.IsNullOrEmpty(ReturnUrl)) + returnUrl = Nav.BaseUri + ReturnUrl.TrimStart('/'); + + await UAuthClient.Flows.BeginPkceAsync(returnUrl); + } + + private async Task ProgrammaticLogin() + { + var deviceId = await DeviceIdProvider.GetOrCreateAsync(); + var request = new LoginRequest + { + Identifier = "admin", + Secret = "admin", + }; + await UAuthClient.Flows.LoginAsync(request, ReturnUrl ?? "/home"); + } + + private async Task HandleLoginResult(IUAuthTryResult result) + { + if (!result.Success) + { + if (result.Reason == AuthFailureReason.LockedOut && result.LockoutUntilUtc is { } until) + { + _lockoutUntil = until; + StartCountdown(); + } + + _remainingAttempts = result.RemainingAttempts; + ShowLoginError(result.Reason, result.RemainingAttempts); + } + } + + private async void StartCountdown() + { + if (_lockoutUntil is null) + return; + + _isLocked = true; + _lockoutStartedAt = DateTimeOffset.UtcNow; + _lockoutDuration = _lockoutUntil.Value - DateTimeOffset.UtcNow; + UpdateRemaining(); + + _lockoutCts?.Cancel(); + _lockoutCts = new CancellationTokenSource(); + + _lockoutTimer?.Dispose(); + _lockoutTimer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + + try + { + while (await _lockoutTimer.WaitForNextTickAsync(_lockoutCts.Token)) + { + UpdateRemaining(); + + if (_remaining <= TimeSpan.Zero) + { + ResetLockoutState(); + await InvokeAsync(StateHasChanged); + break; + } + + await InvokeAsync(StateHasChanged); + } + } + catch (OperationCanceledException) + { + + } + } + + private void ResetLockoutState() + { + _isLocked = false; + _lockoutUntil = null; + _progressPercent = 0; + _remainingAttempts = null; + } + + private void UpdateRemaining() + { + if (_lockoutUntil is null || _lockoutStartedAt is null) + return; + + var now = DateTimeOffset.UtcNow; + + _remaining = _lockoutUntil.Value - now; + + if (_remaining <= TimeSpan.Zero) + { + _remaining = TimeSpan.Zero; + return; + } + + var elapsed = now - _lockoutStartedAt.Value; + + if (_lockoutDuration.TotalSeconds > 0) + { + var percent = 100 - (elapsed.TotalSeconds / _lockoutDuration.TotalSeconds * 100); + _progressPercent = Math.Max(0, percent); + } + } + + private async Task OpenResetDialog() + { + await DialogService.ShowAsync("Reset Credentials", GetDialogParameters(), GetDialogOptions()); + } + + private DialogOptions GetDialogOptions() + { + return new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + CloseButton = true + }; + } + + private DialogParameters GetDialogParameters() + { + return new DialogParameters + { + ["AuthState"] = AuthState + }; + } + + public override void Dispose() + { + base.Dispose(); + _lockoutCts?.Cancel(); + _lockoutTimer?.Dispose(); + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor new file mode 100644 index 00000000..d8eb7138 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor @@ -0,0 +1,27 @@ +@inject NavigationManager Nav + + + + + + + Access Denied + + + You don’t have permission to view this page. + If you think this is a mistake, sign in with a different account or request access. + + + + Sign In + Go Back + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor.cs new file mode 100644 index 00000000..a59522db --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/NotAuthorized.razor.cs @@ -0,0 +1,15 @@ +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Pages; + +public partial class NotAuthorized +{ + private string LoginHref + { + get + { + var returnUrl = Uri.EscapeDataString(Nav.ToBaseRelativePath(Nav.Uri)); + return $"/login?returnUrl=/{returnUrl}"; + } + } + + private void GoBack() => Nav.NavigateTo("/", replace: false); +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor new file mode 100644 index 00000000..bb174660 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor @@ -0,0 +1,61 @@ +@page "/register" +@using CodeBeam.UltimateAuth.Client.Runtime +@inherits UAuthFlowPageBase + +@implements IDisposable +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IUAuthClientProductInfoProvider ClientProductInfoProvider +@inject IDeviceIdProvider DeviceIdProvider +@inject IDialogService DialogService + + + + + + + + + + + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor.cs new file mode 100644 index 00000000..a1917203 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/Register.razor.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Users.Contracts; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Pages; + +public partial class Register +{ + private string? _username; + private string? _password; + private string? _passwordCheck; + private string? _email; + private UAuthClientProductInfo? _productInfo; + private MudForm _form = null!; + + protected override async Task OnInitializedAsync() + { + _productInfo = ClientProductInfoProvider.Get(); + } + + private async Task HandleRegisterAsync() + { + await _form.ValidateAsync(); + + if (!_form.IsValid) + return; + + var request = new CreateUserRequest + { + UserName = _username, + Password = _password, + Email = _email, + }; + + var result = await UAuthClient.Users.CreateAsync(request); + if (result.IsSuccess) + { + Snackbar.Add("User created successfully.", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to create user.", Severity.Error); + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor new file mode 100644 index 00000000..753878b8 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor @@ -0,0 +1,18 @@ +@page "/reset" +@inherits UAuthFlowPageBase + +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar + + + + + + + + + Change Password + + + + \ No newline at end of file diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor.cs new file mode 100644 index 00000000..fc9942f0 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Pages/ResetCredential.razor.cs @@ -0,0 +1,49 @@ +using CodeBeam.UltimateAuth.Credentials.Contracts; +using MudBlazor; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.Pages; + +public partial class ResetCredential +{ + private MudForm _form = null!; + private string? _code; + private string? _newPassword; + private string? _newPasswordCheck; + + private async Task ResetPasswordAsync() + { + await _form.ValidateAsync(); + if (!_form.IsValid) + { + Snackbar.Add("Please fix the validation errors.", Severity.Error); + return; + } + + if (_newPassword != _newPasswordCheck) + { + Snackbar.Add("Passwords do not match.", Severity.Error); + return; + } + + var request = new CompleteResetCredentialRequest + { + ResetToken = _code, + NewSecret = _newPassword ?? string.Empty, + Identifier = Identifier // Coming from UAuthFlowPageBase automatically if begin reset is successful + }; + + var result = await UAuthClient.Credentials.CompleteResetMyAsync(request); + + if (result.IsSuccess) + { + Snackbar.Add("Credential reset successfully. Please log in with your new password.", Severity.Success); + Nav.NavigateTo("/login"); + } + else + { + Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Failed to reset credential. Please try again.", Severity.Error); + } + } + + private string PasswordMatch(string arg) => _newPassword != arg ? "Passwords don't match" : string.Empty; +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs new file mode 100644 index 00000000..adb7d26a --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Program.cs @@ -0,0 +1,33 @@ +using CodeBeam.UAuth.Sample.IntWasm.Client.Infrastructure; +using CodeBeam.UAuth.Sample.IntWasm.Client.ResourceApi; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; +using MudExtensions.Services; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); +builder.Services.AddMudExtensions(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +builder.Services.AddHttpClient("resourceApi", client => +{ + client.BaseAddress = new Uri("https://localhost:6122"); +}); + +builder.Services.AddUltimateAuthClientBlazor(o => +{ + o.Endpoints.BasePath = "https://localhost:6112/auth"; // UAuthHub EFCore URL + o.Reauth.Behavior = ReauthBehavior.RaiseEvent; + o.Login.AllowCredentialPost = true; + o.Pkce.ReturnUrl = "https://localhost:6132/home"; // This application domain + path +}); + +await builder.Build().RunAsync(); diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/ProductApiService.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/ProductApiService.cs new file mode 100644 index 00000000..6220f56b --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/ProductApiService.cs @@ -0,0 +1,78 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using Microsoft.AspNetCore.Components.WebAssembly.Http; +using System.Net.Http.Json; + +namespace CodeBeam.UAuth.Sample.IntWasm.Client.ResourceApi; + +public class ProductApiService +{ + private readonly HttpClient _http; + + public ProductApiService(IHttpClientFactory factory) + { + _http = factory.CreateClient("resourceApi"); + } + + private HttpRequestMessage CreateRequest(HttpMethod method, string url, object? body = null) + { + var request = new HttpRequestMessage(method, url); + request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); + + if (body is not null) + { + request.Content = JsonContent.Create(body); + } + + return request; + } + + public Task>> GetAllAsync() + => SendAsync>(CreateRequest(HttpMethod.Get, "/api/products")); + + public Task> GetAsync(int id) + => SendAsync(CreateRequest(HttpMethod.Get, $"/api/products/{id}")); + + public Task> CreateAsync(SampleProduct product) + => SendAsync(CreateRequest(HttpMethod.Post, $"/api/products", product)); + + public Task> UpdateAsync(int id, SampleProduct product) + => SendAsync(CreateRequest(HttpMethod.Put, $"/api/products/{id}", product)); + + public Task> DeleteAsync(int id) + => SendAsync(CreateRequest(HttpMethod.Delete, $"/api/products/{id}")); + + private async Task> SendAsync(HttpRequestMessage request) + { + var response = await _http.SendAsync(request); + + var result = new UAuthResult + { + Status = (int)response.StatusCode, + IsSuccess = response.IsSuccessStatusCode + }; + + if (response.IsSuccessStatusCode) + { + result.Value = await response.Content.ReadFromJsonAsync(); + return result; + } + + result.Problem = await TryReadProblem(response); + return result; + } + + private async Task TryReadProblem(HttpResponseMessage response) + { + try + { + return await response.Content.ReadFromJsonAsync(); + } + catch + { + return new UAuthProblem + { + Title = response.ReasonPhrase + }; + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/SampleProduct.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/SampleProduct.cs new file mode 100644 index 00000000..b169ecf3 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/ResourceApi/SampleProduct.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UAuth.Sample.IntWasm.Client.ResourceApi; + +public class SampleProduct +{ + public int Id { get; set; } + public string? Name { get; set; } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Routes.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Routes.razor new file mode 100644 index 00000000..e102e257 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/Routes.razor @@ -0,0 +1,73 @@ +@using CodeBeam.UAuth.Sample.IntWasm.Client.Pages +@using CodeBeam.UAuth.Sample.IntWasm.Client.Infrastructure +@inject ISnackbar Snackbar +@inject DarkModeManager DarkModeManager + + + + + + + + + + + + + + + + @* Advanced: you can fully control routing by providing your own Router *@ + @* + + + + + + + + + + + + + + + + *@ + + +@code { + private async Task HandleReauth() + { + Snackbar.Add("Reauthentication required. Please log in again.", Severity.Warning); + } + + #region DarkMode + + protected override void OnInitialized() + { + DarkModeManager.Changed += OnThemeChanged; + } + + private void OnThemeChanged() + { + InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await DarkModeManager.InitializeAsync(); + StateHasChanged(); + } + } + + public void Dispose() + { + DarkModeManager.Changed -= OnThemeChanged; + } + + #endregion +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/_Imports.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/_Imports.razor new file mode 100644 index 00000000..6fb3b530 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/_Imports.razor @@ -0,0 +1,19 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using CodeBeam.UAuth.Sample.IntWasm.Client +@using CodeBeam.UAuth.Sample.IntWasm.Client.Layout +@using CodeBeam.UltimateAuth.Sample +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.Client +@using CodeBeam.UltimateAuth.Client.Blazor + +@using MudBlazor +@using MudExtensions diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.Development.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/wwwroot/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj new file mode 100644 index 00000000..62e3c107 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor new file mode 100644 index 00000000..6ba144f0 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/App.razor @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/Pages/Error.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/Pages/Error.razor new file mode 100644 index 00000000..576cc2d2 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/_Imports.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/_Imports.razor new file mode 100644 index 00000000..ed7e1a53 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using CodeBeam.UAuth.Sample.IntWasm +@using CodeBeam.UAuth.Sample.IntWasm.Client +@using CodeBeam.UAuth.Sample.IntWasm.Components + +@using MudBlazor +@using MudExtensions diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs new file mode 100644 index 00000000..7790e14a --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs @@ -0,0 +1,65 @@ +using CodeBeam.UAuth.Sample.IntWasm.Client.Infrastructure; +using CodeBeam.UAuth.Sample.IntWasm.Client.ResourceApi; +using CodeBeam.UAuth.Sample.IntWasm.Components; +using CodeBeam.UltimateAuth.Client.AspNetCore; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; +using CodeBeam.UltimateAuth.Core.Domain; +using MudBlazor.Services; +using MudExtensions.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); +builder.Services.AddMudExtensions(); + +builder.Services.AddUltimateAuthClientBlazor(o => +{ + o.Endpoints.BasePath = "https://localhost:6112/auth"; // UAuthHub EFCore URL + o.Reauth.Behavior = ReauthBehavior.RaiseEvent; + o.Login.AllowCredentialPost = true; + o.Pkce.ReturnUrl = "https://localhost:6132/home"; // This application domain + path +}); + +builder.Services.AddScoped(); + +builder.Services.AddUltimateAuthAspNetCoreCompatibility(); +// If you want to use the UltimateAuthClientBlazor without the AspNetCore compatibility layer, you can register the services like this instead: +// It gives full AspNetCore compatibility but affects performance significantly. +//builder.Services.AddUltimateAuthResourceApi(o => o.UAuthHubBaseUrl = "https://localhost:6112/auth"); + +builder.Services.AddScoped(); + +builder.Services.AddHttpClient("resourceApi", client => +{ + client.BaseAddress = new Uri("https://localhost:6122"); // Resource API URL +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} +app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); +app.UseHttpsRedirection(); + +//app.UseUltimateAuthResourceApiWithAspNetCore(); +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(CodeBeam.UAuth.Sample.IntWasm.Client._Imports).Assembly, UAuthAssemblies.BlazorClient().First()); + +app.Run(); diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Properties/launchSettings.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Properties/launchSettings.json new file mode 100644 index 00000000..40513f8d --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:6133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:6132;http://localhost:6133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.Development.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/UltimateAuth-Logo.png b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/UltimateAuth-Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7282f15f1b7e435a8e88356ed2351d1edcc26b GIT binary patch literal 14776 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 .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} + +.uauth-stack { + min-height: 60vh; + max-height: calc(100vh - var(--mud-appbar-height)); + width: 30vw; + min-width: 300px; +} + +.uauth-menu-popover { + width: 300px; +} + +.uauth-login-paper { + min-height: 70vh; +} + + .uauth-login-paper.mud-theme-primary { + background: linear-gradient(145deg, var(--mud-palette-primary), rgba(0, 0, 0, 0.85) ); + color: white; + } + +.uauth-brand-glow { + filter: drop-shadow(0 0 25px rgba(255,255,255,0.15)); +} + +.uauth-logo-slide { + animation: uauth-logo-float 30s ease-in-out infinite; +} + +.uauth-text-transform-none .mud-button { + text-transform: none; +} + +.uauth-dialog { + height: 68vh; + max-height: 68vh; + overflow: auto; +} + +.text-secondary { + color: var(--mud-palette-text-secondary); +} + +.uauth-blur { + backdrop-filter: blur(10px); +} + +.uauth-blur-slight { + backdrop-filter: blur(4px); +} + +@keyframes uauth-logo-float { + 0% { + transform: translateY(0) rotateY(0); + } + + 10% { + transform: translateY(0) rotateY(0); + } + + 15% { + transform: translateY(200px) rotateY(360deg); + } + + 35% { + transform: translateY(200px) rotateY(360deg); + } + + 40% { + transform: translateY(200px) rotateY(720deg); + } + + 60% { + transform: translateY(200px) rotateY(720deg); + } + + 65% { + transform: translateY(0) rotateY(360deg); + } + + 85% { + transform: translateY(0) rotateY(360deg); + } + + 90% { + transform: translateY(0) rotateY(0); + } + + 100% { + transform: translateY(0) rotateY(0); + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj new file mode 100644 index 00000000..2d474a92 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.http b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.http new file mode 100644 index 00000000..506fcd1e --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.http @@ -0,0 +1,6 @@ +@CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore_HostAddress = http://localhost:5151 + +GET {{CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Program.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Program.cs new file mode 100644 index 00000000..b1825cb2 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Program.cs @@ -0,0 +1,28 @@ +using CodeBeam.UltimateAuth.Server.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +builder.Services.AddUltimateAuthResourceApi(o => +{ + o.UAuthHubBaseUrl = "https://localhost:6112"; + o.AllowedClientOrigins.Add("https://localhost:6132"); + o.CorsPolicyName = "resourceApi"; +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseUltimateAuthResourceApiWithAspNetCore(); + +app.MapControllers(); + +app.Run(); diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Properties/launchSettings.json b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Properties/launchSettings.json new file mode 100644 index 00000000..00407688 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:6123", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:6122;http://localhost:6123", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/AppActions.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/AppActions.cs new file mode 100644 index 00000000..7d9191f3 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/AppActions.cs @@ -0,0 +1,18 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Defaults; + +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +public static class AppActions +{ + public static class Products + { + public static readonly string Read = UAuthActions.Create("products", "read", ActionScope.Self); + + public static readonly string Create = UAuthActions.Create("products", "create", ActionScope.Admin); + + public static readonly string Update = UAuthActions.Create("products", "update", ActionScope.Admin); + + public static readonly string Delete = UAuthActions.Create("products", "delete", ActionScope.Admin); + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductStore.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductStore.cs new file mode 100644 index 00000000..10db05bf --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductStore.cs @@ -0,0 +1,6 @@ +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +public static class ProductStore +{ + public static List Items = new() { new SampleProduct() { Id = 0, Name = "Test"} }; +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductsController.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductsController.cs new file mode 100644 index 00000000..8f99886a --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductsController.cs @@ -0,0 +1,73 @@ +using CodeBeam.UltimateAuth.Core.Defaults; +using CodeBeam.UltimateAuth.Core.Errors; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +[ApiController] +[Route("api/products")] +public class ProductsController : ControllerBase +{ + [HttpGet] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] // You can use UAuthActions as permission in ASP.NET Core policy. + public IActionResult GetAll() + { + return Ok(ProductStore.Items); + } + + [HttpGet("{id}")] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Get(int id) + { + var item = ProductStore.Items.FirstOrDefault(x => x.Id == id); + if (item == null) return NotFound(); + + return Ok(item); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Create(SampleProduct product) + { + var nextId = ProductStore.Items.Any() + ? ProductStore.Items.Max(x => x.Id) + 1 + : 1; + + product.Id = nextId; + ProductStore.Items.Add(product); + + return Ok(product); + } + + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Update(int id, SampleProduct product) + { + var item = ProductStore.Items.FirstOrDefault(x => x.Id == id); + + if (item == null) + { + throw new UAuthNotFoundException("No product found."); + } + + item.Name = product.Name; + return Ok(product); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Delete(int id) + { + var item = ProductStore.Items.FirstOrDefault(x => x.Id == id); + if (item == null) return NotFound(); + + ProductStore.Items.Remove(item); + return Ok(item); + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/SampleProduct.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/SampleProduct.cs new file mode 100644 index 00000000..2c75603d --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/SampleProduct.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +public class SampleProduct +{ + public int Id { get; set; } + public string Name { get; set; } = default!; +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.Development.json b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.json b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs index a59b1f61..0b595429 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs @@ -7,6 +7,7 @@ builder.Services.AddUltimateAuthResourceApi(o => { + // TODO: Make multiple UAuthHub support via resolver, then different client apps can use different UAuthHub instances if needed. o.UAuthHubBaseUrl = "https://localhost:6110"; o.AllowedClientOrigins.Add("https://localhost:6130"); }); diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs index d6986a1e..290b399e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs @@ -77,7 +77,10 @@ UAuthMode.SemiHybrid or if (persistence == RefreshTokenPersistence.Persist) { var store = _storeFactory.Create(flow.Tenant); - await store.StoreAsync(stored, ct); + await store.ExecuteAsync(async ct => + { + await store.StoreAsync(stored, ct); + }); } return new RefreshTokenInfo diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs index 3a0b5a30..b6f8d893 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs @@ -214,9 +214,7 @@ public async Task> GetByIdsAsync( return result.AsReadOnly(); } - public async Task> QueryAsync( - RoleQuery query, - CancellationToken ct = default) + public async Task> QueryAsync(RoleQuery query, CancellationToken ct = default) { var normalized = query.Normalize(); @@ -257,8 +255,25 @@ public async Task> QueryAsync( .Take(normalized.PageSize) .ToListAsync(ct); + var roleIds = items.Select(x => x.Id).ToList(); + + var permissions = await DbSetPermission + .AsNoTracking() + .Where(x => + x.Tenant == _tenant && + roleIds.Contains(x.RoleId)) + .ToListAsync(ct); + + var lookup = permissions + .GroupBy(x => x.RoleId) + .ToDictionary(x => x.Key); + var result = items - .Select(x => RoleMapper.ToDomain(x, Enumerable.Empty())) + .Select(x => + { + lookup.TryGetValue(x.Id, out var perms); + return RoleMapper.ToDomain(x, perms ?? Enumerable.Empty()); + }) .ToList() .AsReadOnly(); diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj new file mode 100644 index 00000000..a6b3ab90 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj @@ -0,0 +1,28 @@ + + + + net8.0;net9.0;net10.0 + $(NoWarn);1591 + + CodeBeam.UltimateAuth.Client.AspNetCore + + + Provides ASP.NET Core compatibility layer for UltimateAuth client applications. + This package enables integration with ASP.NET Core authentication and authorization infrastructure without introducing server-side authentication overhead. It allows applications to use features like [Authorize] without requiring a full server-side auth pipeline. + Designed for Blazor Web App (Interactive WASM) and similar hybrid hosting scenarios. + This package does NOT implement authentication. It only provides a lightweight compatibility layer. + + authentication;authorization;identity;aspnetcore;auth;jwt;auth-framework + uauthlogo.png + README.md + + + + + + + + + + + diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/NoOpAuthHandler.cs b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/NoOpAuthHandler.cs new file mode 100644 index 00000000..1c92c535 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/NoOpAuthHandler.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Encodings.Web; + +namespace CodeBeam.UltimateAuth.Client.AspNetCore; + +public class NoopAuthHandler : AuthenticationHandler +{ + public NoopAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/README.md b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/README.md new file mode 100644 index 00000000..8a3bad2a --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/README.md @@ -0,0 +1,30 @@ +# UltimateAuth Client ASP.NET Core Compatibility + +Provides a lightweight ASP.NET Core compatibility layer for UltimateAuth client applications. + +--- + +## 🎯 Purpose + +This package enables seamless integration between **UltimateAuth client-side authentication** and **ASP.NET Core infrastructure**. + +It allows applications to: + +- Use `[Authorize]` without runtime errors +- Integrate with ASP.NET Core middleware pipeline +- Avoid mandatory server-side authentication setup + +--- + +## ⚠️ Important + +This package does **NOT** perform authentication. + +It only provides a **No-op compatibility layer** for ASP.NET Core. + +```text +✔ Prevents ASP.NET Core auth errors +✔ Enables framework compatibility +❌ Does NOT validate users +❌ Does NOT populate HttpContext.User +❌ Does NOT enforce security \ No newline at end of file diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/ServiceCollectionExtensions.cs b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b546b9d5 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.Client.AspNetCore; + +public static class UAuthAspNetCoreExtensions +{ + public static IServiceCollection AddUltimateAuthAspNetCoreCompatibility( + this IServiceCollection services) + { + services.AddAuthentication("UAuth.Noop") + .AddScheme("UAuth.Noop", _ => { }); + + services.AddAuthorization(); + + return services; + } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/uauthlogo.png b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/uauthlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..911f2530a1353be33b73719b23571bd6edfb6e96 GIT binary patch literal 4954 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 Date: Tue, 14 Apr 2026 00:18:51 +0300 Subject: [PATCH 2/2] Project Arrangement & Docs Dependency Update (#39) --- UltimateAuth.slnx | 6 +-- ...eBeam.UltimateAuth.Docs.Wasm.Client.csproj | 4 +- ...mateAuth.EntityFrameworkCore.Bundle.csproj | 35 ----------------- .../CodeBeam.UltimateAuth.Sample.Seed.csproj | 2 +- ...UltimateAuth.Sample.UAuthHub.EFCore.csproj | 2 +- ...deBeam.UltimateAuth.Sample.UAuthHub.csproj | 6 +-- ...mateAuth.Sample.BlazorServer.EFCore.csproj | 8 ++-- ...am.UltimateAuth.Sample.BlazorServer.csproj | 6 +-- ...ateAuth.Sample.BlazorStandaloneWasm.csproj | 4 +- .../CodeBeam.UAuth.Sample.IntWasm.csproj | 1 - ...mateAuth.EntityFrameworkCore.Bundle.csproj | 36 ++++++++++++++++++ .../Data/UAuthDbContext.cs | 0 ...timateAuthEntityFrameworkCoreExtensions.cs | 0 .../Options/UAuthEfCoreOptions.cs | 0 .../README.md | 0 .../uauthlogo.png | Bin ...deBeam.UltimateAuth.InMemory.Bundle.csproj | 12 +++--- .../README.md | 0 .../UltimateAuthInMemoryExtensions.cs | 0 .../uauthlogo.png | Bin ...eBeam.UltimateAuth.Reference.Bundle.csproj | 8 ++-- .../README.md | 0 .../UltimateAuthReferenceBundleExtensions.cs | 0 .../uauthlogo.png | Bin .../CodeBeam.UltimateAuth.Tests.Unit.csproj | 4 +- 25 files changed, 67 insertions(+), 67 deletions(-) delete mode 100644 nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj create mode 100644 src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj rename {nuget/CodeBeam.UltimateAuth.EntityFrameworkCore => src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle}/Data/UAuthDbContext.cs (100%) rename {nuget/CodeBeam.UltimateAuth.EntityFrameworkCore => src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle}/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs (100%) rename {nuget/CodeBeam.UltimateAuth.EntityFrameworkCore => src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle}/Options/UAuthEfCoreOptions.cs (100%) rename {nuget/CodeBeam.UltimateAuth.EntityFrameworkCore => src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle}/README.md (100%) rename {nuget/CodeBeam.UltimateAuth.EntityFrameworkCore => src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle}/uauthlogo.png (100%) rename {nuget/CodeBeam.UltimateAuth.InMemory => src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle}/CodeBeam.UltimateAuth.InMemory.Bundle.csproj (55%) rename {nuget/CodeBeam.UltimateAuth.InMemory => src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle}/README.md (100%) rename {nuget/CodeBeam.UltimateAuth.InMemory => src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle}/UltimateAuthInMemoryExtensions.cs (100%) rename {nuget/CodeBeam.UltimateAuth.InMemory => src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle}/uauthlogo.png (100%) rename {nuget => src/bundle}/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj (66%) rename {nuget => src/bundle}/CodeBeam.UltimateAuth.Reference.Bundle/README.md (100%) rename {nuget => src/bundle}/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs (100%) rename {nuget => src/bundle}/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png (100%) diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx index fb479af0..9bef746e 100644 --- a/UltimateAuth.slnx +++ b/UltimateAuth.slnx @@ -4,9 +4,9 @@
- - - + + + diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj index 3ac3c530..3e3e1fb5 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj @@ -11,10 +11,10 @@ - + - + diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj deleted file mode 100644 index 29faf8f7..00000000 --- a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net8.0;net9.0;net10.0 - $(NoWarn);1591 - - CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle - - - Provides a complete Entity Framework Core persistence setup for UltimateAuth. - This package includes reference domain implementations and Entity Framework Core-based persistence for all modules. - It is designed for production scenarios requiring durable storage. - - - authentication;authorization;identity;efcore;inmemory;bundle;auth-framework;security;jwt - uauthlogo.png - README.md - - - - - - - - - - - - - - - - - - diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj index 96d9adaa..92487581 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj index f79f1a74..f6e72309 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj @@ -25,7 +25,7 @@ - + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj index b7cf0040..a5ccdc5e 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj @@ -9,14 +9,14 @@ - + - + - + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj index f49ac077..90bfe9d6 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -19,13 +19,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj index 9d71e7d7..daf780dd 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj @@ -9,13 +9,13 @@ - - + + - + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj index a77dd098..4b88fd56 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj @@ -8,12 +8,12 @@ - + - + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj index 62e3c107..f309e0b0 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj @@ -10,7 +10,6 @@ - diff --git a/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj new file mode 100644 index 00000000..17220e06 --- /dev/null +++ b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj @@ -0,0 +1,36 @@ + + + + net8.0;net9.0;net10.0 + $(NoWarn);1591 + + CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle + + + Provides a complete Entity Framework Core persistence setup for UltimateAuth. + This package includes reference domain implementations and Entity Framework Core-based persistence for all modules. + It is designed for production scenarios requiring durable storage. + + + authentication;authorization;identity;efcore;inmemory;bundle;auth-framework;security;jwt + uauthlogo.png + README.md + + + + + + + + + + + + + + + + + + + diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Data/UAuthDbContext.cs b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Data/UAuthDbContext.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Data/UAuthDbContext.cs rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Data/UAuthDbContext.cs diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Options/UAuthEfCoreOptions.cs b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Options/UAuthEfCoreOptions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Options/UAuthEfCoreOptions.cs rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Options/UAuthEfCoreOptions.cs diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/README.md b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/README.md similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/README.md rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/README.md diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/uauthlogo.png b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/uauthlogo.png similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/uauthlogo.png rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/uauthlogo.png diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj similarity index 55% rename from nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.Bundle.csproj rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj index 45fc3e18..c0c22ad8 100644 --- a/nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.Bundle.csproj +++ b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj @@ -20,12 +20,12 @@ - - - - - - + + + + + + diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/README.md b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/README.md similarity index 100% rename from nuget/CodeBeam.UltimateAuth.InMemory/README.md rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/README.md diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/UltimateAuthInMemoryExtensions.cs b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/UltimateAuthInMemoryExtensions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.InMemory/UltimateAuthInMemoryExtensions.cs rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/UltimateAuthInMemoryExtensions.cs diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/uauthlogo.png b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/uauthlogo.png similarity index 100% rename from nuget/CodeBeam.UltimateAuth.InMemory/uauthlogo.png rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/uauthlogo.png diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj similarity index 66% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj index e8cde4d1..01f6356a 100644 --- a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj +++ b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/README.md b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/README.md similarity index 100% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/README.md rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/README.md diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png similarity index 100% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj index ee936bd7..1b44b87b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj @@ -20,8 +20,8 @@ - - + +