diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Brand/UAuthLogo.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Brand/UAuthLogo.razor new file mode 100644 index 00000000..2806b7d3 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/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/Brand/UAuthLogo.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Brand/UAuthLogo.razor.cs new file mode 100644 index 00000000..030d9b66 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/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/Brand/UAuthLogoVariant.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Brand/UAuthLogoVariant.cs new file mode 100644 index 00000000..fe3be220 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/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/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj index 32b6af51..fd8d083e 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,8 +9,10 @@ - - + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor index 7f12ea3d..f9989cb9 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor @@ -6,11 +6,11 @@ - + @* *@ - + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor index 3202e106..d8673b17 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor @@ -31,10 +31,26 @@ @if (_state == null || !_state.IsActive) { - - This page cannot be accessed directly. - UAuthHub login flows can only be initiated by an authorized client application. - + + + + + + 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; } diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs index 31199c37..dc0f988e 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs @@ -6,131 +6,130 @@ using Microsoft.AspNetCore.WebUtilities; using MudBlazor; -namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Pages +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Pages; + +public partial class Home { - public partial class Home - { - [SupplyParameterFromQuery(Name = "hub")] - public string? HubKey { get; set; } + [SupplyParameterFromQuery(Name = "hub")] + public string? HubKey { get; set; } - private string? _username; - private string? _password; + private string? _username; + private string? _password; - private HubFlowState? _state; + private HubFlowState? _state; - protected override async Task OnParametersSetAsync() + protected override async Task OnParametersSetAsync() + { + if (string.IsNullOrWhiteSpace(HubKey)) { - if (string.IsNullOrWhiteSpace(HubKey)) - { - _state = null; - return; - } - - if (!HubSessionId.TryParse(HubKey, out var hubSessionId)) - _state = await HubFlowReader.GetStateAsync(hubSessionId); + _state = null; + return; } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; - - var currentError = await BrowserStorage.GetAsync(StorageScope.Session, "uauth:last_error"); - - if (!string.IsNullOrWhiteSpace(currentError)) - { - Snackbar.Add(ResolveErrorMessage(currentError), Severity.Error); - await BrowserStorage.RemoveAsync(StorageScope.Session, "uauth:last_error"); - } - - var uri = Nav.ToAbsoluteUri(Nav.Uri); - var query = QueryHelpers.ParseQuery(uri.Query); - - if (query.TryGetValue("__uauth_error", out var error)) - { - await BrowserStorage.SetAsync(StorageScope.Session, "uauth:last_error", error.ToString()); - } - - if (string.IsNullOrWhiteSpace(HubKey)) - { - return; - } - - if (_state is null || !_state.Exists) - return; - - if (_state?.IsActive != true) - { - await StartNewPkceAsync(); - return; - } - } + if (HubSessionId.TryParse(HubKey, out var hubSessionId)) + _state = await HubFlowReader.GetStateAsync(hubSessionId); + } - // For testing & debugging - private async Task ProgrammaticPkceLogin() - { - var hub = _state; + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; - if (hub is null) - return; + var currentError = await BrowserStorage.GetAsync(StorageScope.Session, "uauth:last_error"); - if (!HubSessionId.TryParse(HubKey, out var hubSessionId)) - return; + if (!string.IsNullOrWhiteSpace(currentError)) + { + Snackbar.Add(ResolveErrorMessage(currentError), Severity.Error); + await BrowserStorage.RemoveAsync(StorageScope.Session, "uauth:last_error"); + } - var credentials = await HubCredentialResolver.ResolveAsync(hubSessionId); + var uri = Nav.ToAbsoluteUri(Nav.Uri); + var query = QueryHelpers.ParseQuery(uri.Query); - var request = new PkceLoginRequest - { - Identifier = "admin", - Secret = "admin", - AuthorizationCode = credentials?.AuthorizationCode ?? string.Empty, - CodeVerifier = credentials?.CodeVerifier ?? string.Empty, - ReturnUrl = _state?.ReturnUrl ?? string.Empty - }; - await UAuthClient.Flows.CompletePkceLoginAsync(request); + if (query.TryGetValue("__uauth_error", out var error)) + { + await BrowserStorage.SetAsync(StorageScope.Session, "uauth:last_error", error.ToString()); + } + + if (string.IsNullOrWhiteSpace(HubKey)) + { + return; } - private async Task StartNewPkceAsync() + if (_state is null || !_state.Exists) + return; + + if (_state?.IsActive != true) { - var returnUrl = await ResolveReturnUrlAsync(); - await UAuthClient.Flows.BeginPkceAsync(returnUrl); + await StartNewPkceAsync(); + return; } + } - private async Task ResolveReturnUrlAsync() + // For testing & debugging + private async Task ProgrammaticPkceLogin() + { + var hub = _state; + + if (hub is null) + return; + + if (!HubSessionId.TryParse(HubKey, out var hubSessionId)) + return; + + var credentials = await HubCredentialResolver.ResolveAsync(hubSessionId); + + var request = new PkceLoginRequest { - var fromContext = _state?.ReturnUrl; - if (!string.IsNullOrWhiteSpace(fromContext)) - return fromContext; + Identifier = "admin", + Secret = "admin", + AuthorizationCode = credentials?.AuthorizationCode ?? string.Empty, + CodeVerifier = credentials?.CodeVerifier ?? string.Empty, + ReturnUrl = _state?.ReturnUrl ?? string.Empty + }; + await UAuthClient.Flows.CompletePkceLoginAsync(request); + } - var uri = Nav.ToAbsoluteUri(Nav.Uri); - var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query); + private async Task StartNewPkceAsync() + { + var returnUrl = await ResolveReturnUrlAsync(); + await UAuthClient.Flows.BeginPkceAsync(returnUrl); + } - if (query.TryGetValue("return_url", out var ru) && !string.IsNullOrWhiteSpace(ru)) - return ru!; + private async Task ResolveReturnUrlAsync() + { + var fromContext = _state?.ReturnUrl; + if (!string.IsNullOrWhiteSpace(fromContext)) + return fromContext; - 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!; - } + var uri = Nav.ToAbsoluteUri(Nav.Uri); + var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query); - // Config default (recommend adding to options) - //if (!string.IsNullOrWhiteSpace(_options.Login.DefaultReturnUrl)) - // return _options.Login.DefaultReturnUrl!; + if (query.TryGetValue("return_url", out var ru) && !string.IsNullOrWhiteSpace(ru)) + return ru!; - return Nav.Uri; - } - - private string ResolveErrorMessage(string? errorKey) + if (query.TryGetValue("hub", out var hubKey) && !string.IsNullOrWhiteSpace(hubKey)) { - if (errorKey == "invalid") - { - return "Login failed."; - } + var artifact = await AuthStore.GetAsync(new AuthArtifactKey(hubKey!)); + if (artifact is HubFlowArtifact flow && !string.IsNullOrWhiteSpace(flow.ReturnUrl)) + return flow.ReturnUrl!; + } + + // Config default (recommend adding to options) + //if (!string.IsNullOrWhiteSpace(_options.Login.DefaultReturnUrl)) + // return _options.Login.DefaultReturnUrl!; - return "Failed attempt."; + return Nav.Uri; + } + + private string ResolveErrorMessage(string? errorKey) + { + if (errorKey == "invalid") + { + return "Login failed."; } + return "Failed attempt."; } + } diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/NotAuthorized.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/NotAuthorized.razor new file mode 100644 index 00000000..2c0e9b77 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/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/Components/Pages/NotAuthorized.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/NotAuthorized.razor.cs new file mode 100644 index 00000000..4e750030 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/NotAuthorized.razor.cs @@ -0,0 +1,15 @@ +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.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/Components/Routes.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Routes.razor index f06c25ba..9e918850 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Routes.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Routes.razor @@ -1,14 +1,59 @@ - - - - - - - - - - - - - +@using CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Pages +@using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure +@inject ISnackbar Snackbar +@inject DarkModeManager DarkModeManager + + + + + + + + + + + + + + + + + + + + +@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/Controllers/HubLoginController.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs index fa7a1ae5..71cb29b3 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs @@ -9,7 +9,7 @@ namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Controllers; -[Route("uauthhub")] +[Route("auth/uauthhub")] [IgnoreAntiforgeryToken] public sealed class HubLoginController : Controller { diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Infrastructure/DarkModeManager.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Infrastructure/DarkModeManager.cs new file mode 100644 index 00000000..f8f05cb4 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Infrastructure/DarkModeManager.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure; + +public sealed class DarkModeManager +{ + private const string StorageKey = "uauth:theme:dark"; + + private readonly IBrowserStorage _storage; + + public DarkModeManager(IBrowserStorage 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/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs index f3f88149..b0412644 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs @@ -1,22 +1,24 @@ using CodeBeam.UltimateAuth.Authentication.InMemory; -using CodeBeam.UltimateAuth.Authorization.InMemory; using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions; using CodeBeam.UltimateAuth.Authorization.Reference.Extensions; +using CodeBeam.UltimateAuth.Client; using CodeBeam.UltimateAuth.Client.Extensions; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.Runtime; using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions; using CodeBeam.UltimateAuth.Credentials.Reference; using CodeBeam.UltimateAuth.Sample.UAuthHub.Components; +using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure; using CodeBeam.UltimateAuth.Security.Argon2; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Sessions.InMemory; using CodeBeam.UltimateAuth.Tokens.InMemory; using CodeBeam.UltimateAuth.Users.InMemory.Extensions; -using CodeBeam.UltimateAuth.Users.Reference; using CodeBeam.UltimateAuth.Users.Reference.Extensions; using MudBlazor.Services; using MudExtensions.Services; +using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); @@ -26,18 +28,11 @@ builder.Services.AddControllers(); -builder.Services.AddMudServices(); +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); builder.Services.AddMudExtensions(); -//builder.Services -// .AddAuthentication(options => -// { -// options.DefaultAuthenticateScheme = UAuthSchemeDefaults.AuthenticationScheme; -// options.DefaultSignInScheme = UAuthSchemeDefaults.AuthenticationScheme; -// options.DefaultChallengeScheme = UAuthSchemeDefaults.AuthenticationScheme; -// }) -// .AddUAuthCookies(); - //builder.Services.AddAuthorization(); //builder.Services.AddHttpContextAccessor(); @@ -66,6 +61,7 @@ }); builder.Services.AddSingleton(); +builder.Services.AddScoped(); builder.Services.AddCors(options => { @@ -75,33 +71,29 @@ .WithOrigins("https://localhost:6130") .AllowAnyHeader() .AllowAnyMethod() - .AllowCredentials(); + .AllowCredentials() + .WithExposedHeaders("X-UAuth-Refresh"); // TODO: Add exposed headers globally }); }); var app = builder.Build(); -using (var scope = app.Services.CreateScope()) -{ - scope.ServiceProvider.GetRequiredService(); - scope.ServiceProvider.GetRequiredService(); - scope.ServiceProvider.GetRequiredService(); - - var seeder = scope.ServiceProvider.GetService(); - //if (seeder is not null) - // await seeder.SeedAsync(); - - -} - -// Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } -//app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); +else +{ + app.MapOpenApi(); + app.MapScalarApiReference(); + + using var scope = app.Services.CreateScope(); + var seedRunner = scope.ServiceProvider.GetRequiredService(); + await seedRunner.RunAsync(null); +} + app.UseHttpsRedirection(); app.UseCors("WasmSample"); @@ -113,7 +105,8 @@ app.MapControllers(); app.MapRazorComponents() - .AddInteractiveServerRenderMode(); + .AddInteractiveServerRenderMode() + .AddUltimateAuthClientRoutes(typeof(UAuthClientMarker).Assembly); app.MapGet("/health", () => { diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/UltimateAuth-Logo.png b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/UltimateAuth-Logo.png new file mode 100644 index 00000000..5b7282f1 Binary files /dev/null and b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/UltimateAuth-Logo.png differ diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css index 73a69d6f..671b6199 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css @@ -57,4 +57,96 @@ h1:focus { .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { text-align: start; -} \ No newline at end of file +} + +.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/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/favicon.png b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/favicon.png deleted file mode 100644 index 8422b596..00000000 Binary files a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/favicon.png and /dev/null differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Brand/UAuthLogo.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Brand/UAuthLogo.razor index da3cf268..2806b7d3 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Brand/UAuthLogo.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Brand/UAuthLogo.razor @@ -1,33 +1,19 @@ @namespace CodeBeam.UltimateAuth.Sample @inherits ComponentBase - + @if (Variant == UAuthLogoVariant.Brand) { + d="M32.39,14.07H167.61c11.27,0,18,6.76,18,18V133.52c0,22.54-58.59,69.87-85.64,92.41-27-22.54-85.64-69.87-85.64-92.41V32.1C14.36,20.83,21.12,14.07,32.39,14.07Z" /> - + } else { - + - + } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor index 7873ce91..0c91e45c 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor @@ -4,7 +4,7 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService - + Identifier Management User: @AuthState?.Identity?.DisplayName @@ -21,73 +21,3 @@ - -@code { - [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 = SelfUserStatus.SelfSuspended }; - var result = await UAuthClient.Users.ChangeStatusSelfAsync(request); - if (result.IsSuccess) - { - Snackbar.Add("Your account suspended successfully.", Severity.Success); - MudDialog.Close(); - } - else - { - Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "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?.Problem?.Detail ?? result?.Problem?.Title ?? "Delete failed.", Severity.Error); - } - } -} \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor.cs new file mode 100644 index 00000000..64797ba4 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/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.UltimateAuth.Sample.BlazorServer.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 = SelfUserStatus.SelfSuspended }; + var result = await UAuthClient.Users.ChangeStatusSelfAsync(request); + if (result.IsSuccess) + { + Snackbar.Add("Your account suspended successfully.", Severity.Success); + MudDialog.Close(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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?.GetErrorText ?? "Delete failed.", Severity.Error); + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor index 479a08bd..9a514935 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor @@ -9,12 +9,12 @@ - + - + @@ -25,52 +25,3 @@ Create - -@code { - 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.Validate(); - - if (!_form.IsValid) - return; - - if (_password != _passwordCheck) - { - Snackbar.Add("Passwords do not match", Severity.Error); - return; - } - - var request = new CreateUserRequest - { - UserName = _username, - Email = _email, - DisplayName = _displayName, - Password = _password - }; - - var result = await UAuthClient.Users.CreateAdminAsync(request); - - if (!result.IsSuccess) - { - Snackbar.Add(result.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor.cs new file mode 100644 index 00000000..bb7998b1 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor.cs @@ -0,0 +1,55 @@ +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.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.Validate(); + + 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.CreateAdminAsync(request); + + if (!result.IsSuccess) + { + Snackbar.Add(result.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor index 01c01ff0..660b7c3a 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor @@ -49,86 +49,3 @@ Cancel - -@code { - 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.Validate(); - 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.ChangeCredentialAsync(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.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor.cs new file mode 100644 index 00000000..1f207f8d --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/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.UltimateAuth.Sample.BlazorServer.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.Validate(); + 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.ChangeCredentialAsync(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.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor index 808e7fda..0d631533 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor @@ -19,7 +19,7 @@ - @@ -73,7 +73,7 @@ - + @@ -104,303 +104,3 @@ Cancel - -@code { - private MudDataGrid? _grid; - private UserIdentifierType _newIdentifierType; - private string? _newIdentifierValue; - private bool _newIdentifierPrimary; - private bool _loading = false; - private bool _reloadQueued; - - [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) - { - var result = await UAuthClient.Identifiers.GetMyIdentifiersAsync(); - 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.GetMyIdentifiersAsync(req); - } - else - { - res = await UAuthClient.Identifiers.GetUserIdentifiersAsync(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(UserIdentifierDto item) - { - UpdateUserIdentifierRequest updateRequest = new() - { - Id = item.Id, - NewValue = item.Value - }; - - UAuthResult result; - - if (UserKey is null) - { - result = await UAuthClient.Identifiers.UpdateSelfAsync(updateRequest); - } - else - { - result = await UAuthClient.Identifiers.UpdateAdminAsync(UserKey.Value, updateRequest); - } - - if (result.IsSuccess) - { - Snackbar.Add("Identifier updated successfully", Severity.Success); - } - else - { - Snackbar.Add(result?.GetErrorText ?? "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.AddSelfAsync(request); - } - else - { - result = await UAuthClient.Identifiers.AddAdminAsync(UserKey.Value, request); - } - - if (result.IsSuccess) - { - Snackbar.Add("Identifier added successfully", Severity.Success); - await ReloadAsync(); - StateHasChanged(); - } - else - { - Snackbar.Add(result?.GetErrorText ?? "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() { IdentifierId = id }; - UAuthResult result; - - if (UserKey is null) - { - result = await UAuthClient.Identifiers.VerifySelfAsync(request); - } - else - { - result = await UAuthClient.Identifiers.VerifyAdminAsync(UserKey.Value, request); - } - - if (result.IsSuccess) - { - Snackbar.Add("Identifier verified successfully", Severity.Success); - await ReloadAsync(); - StateHasChanged(); - } - else - { - Snackbar.Add(result?.GetErrorText ?? "Failed to verify primary identifier", Severity.Error); - } - } - - private async Task SetPrimaryAsync(Guid id) - { - SetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; - UAuthResult result; - - if (UserKey is null) - { - result = await UAuthClient.Identifiers.SetPrimarySelfAsync(request); - } - else - { - result = await UAuthClient.Identifiers.SetPrimaryAdminAsync(UserKey.Value, request); - } - - if (result.IsSuccess) - { - Snackbar.Add("Primary identifier set successfully", Severity.Success); - await ReloadAsync(); - StateHasChanged(); - } - else - { - Snackbar.Add(result?.GetErrorText ?? "Failed to set primary identifier", Severity.Error); - } - } - - private async Task UnsetPrimaryAsync(Guid id) - { - UnsetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; - UAuthResult result; - - if (UserKey is null) - { - result = await UAuthClient.Identifiers.UnsetPrimarySelfAsync(request); - } - else - { - result = await UAuthClient.Identifiers.UnsetPrimaryAdminAsync(UserKey.Value, request); - } - - if (result.IsSuccess) - { - Snackbar.Add("Primary identifier unset successfully", Severity.Success); - await ReloadAsync(); - StateHasChanged(); - } - else - { - Snackbar.Add(result?.GetErrorText ?? "Failed to unset primary identifier", Severity.Error); - } - } - - private async Task DeleteIdentifier(Guid id) - { - DeleteUserIdentifierRequest request = new() { IdentifierId = id }; - UAuthResult result; - - if (UserKey is null) - { - result = await UAuthClient.Identifiers.DeleteSelfAsync(request); - } - else - { - result = await UAuthClient.Identifiers.DeleteAdminAsync(UserKey.Value, request); - } - - if (result.IsSuccess) - { - Snackbar.Add("Identifier deleted successfully", Severity.Success); - await ReloadAsync(); - StateHasChanged(); - } - else - { - Snackbar.Add(result?.GetErrorText ?? "Failed to delete identifier", Severity.Error); - } - } - - private void Cancel() => MudDialog.Cancel(); -} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor.cs new file mode 100644 index 00000000..4c789ba6 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor.cs @@ -0,0 +1,309 @@ +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.UltimateAuth.Sample.BlazorServer.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; + + [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) + { + var result = await UAuthClient.Identifiers.GetMyIdentifiersAsync(); + 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.GetMyIdentifiersAsync(req); + } + else + { + res = await UAuthClient.Identifiers.GetUserIdentifiersAsync(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.UpdateSelfAsync(updateRequest); + } + else + { + result = await UAuthClient.Identifiers.UpdateAdminAsync(UserKey.Value, updateRequest); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier updated successfully", Severity.Success); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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.AddSelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.AddAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier added successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.VerifySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.VerifyAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier verified successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to verify primary identifier", Severity.Error); + } + } + + private async Task SetPrimaryAsync(Guid id) + { + SetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.SetPrimarySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.SetPrimaryAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier set successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to set primary identifier", Severity.Error); + } + } + + private async Task UnsetPrimaryAsync(Guid id) + { + UnsetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.UnsetPrimarySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.UnsetPrimaryAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier unset successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to unset primary identifier", Severity.Error); + } + } + + private async Task DeleteIdentifier(Guid id) + { + DeleteUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.DeleteSelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.DeleteAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier deleted successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to delete identifier", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor index 77cf7c13..8e0df863 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor @@ -44,117 +44,3 @@ Save - -@code { - - [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(); - } - - void ToggleGroup(PermissionGroup group, bool value) - { - foreach (var item in group.Items) - item.Selected = value; - } - - void TogglePermission(PermissionItem item, bool value) - { - item.Selected = value; - } - - 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 SetPermissionsRequest - { - Permissions = permissions - }; - - var result = await UAuthClient.Authorization.SetPermissionsAsync(Role.Id, req); - - if (!result.IsSuccess) - { - Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "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; } - } -} \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor.cs new file mode 100644 index 00000000..844afd4f --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor.cs @@ -0,0 +1,119 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.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 SetPermissionsRequest + { + Permissions = permissions + }; + + var result = await UAuthClient.Authorization.SetPermissionsAsync(Role.Id, req); + + if (!result.IsSuccess) + { + Snackbar.Add(result.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor index 7a02975b..d09fcfa0 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor @@ -19,7 +19,7 @@ - + @@ -92,108 +92,3 @@ Save - -@code { - 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; - - [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.GetProfileAsync(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; - } - } - - 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.Validate(); - 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.UpdateProfileAsync(UserKey.Value, request); - } - - if (result.IsSuccess) - { - Snackbar.Add("Profile updated", Severity.Success); - MudDialog.Close(DialogResult.Ok(true)); - } - else - { - Snackbar.Add(result.GetErrorText ?? "Failed to update profile", Severity.Error); - } - } - - private void Cancel() => MudDialog.Cancel(); -} \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor.cs new file mode 100644 index 00000000..5b861925 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor.cs @@ -0,0 +1,114 @@ +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.UltimateAuth.Sample.BlazorServer.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; + + [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.GetProfileAsync(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; + } + } + + 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.Validate(); + 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.UpdateProfileAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Profile updated", Severity.Success); + MudDialog.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add(result.GetErrorText ?? "Failed to update profile", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor index 1e909a9f..06a515aa 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor @@ -33,43 +33,6 @@ - Cancel - OK + Close - -@code { - 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 BeginCredentialResetRequest - { - 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.Problem?.Detail ?? result.Problem?.Title ?? "Failed to request credential reset.", Severity.Error); - return; - } - - _resetCode = result.Value.Token; - _resetRequested = true; - } - - private void Submit() => MudDialog.Close(DialogResult.Ok(true)); - - private void Cancel() => MudDialog.Cancel(); -} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor.cs new file mode 100644 index 00000000..c719539b --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/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.UltimateAuth.Sample.BlazorServer.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 BeginCredentialResetRequest + { + 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.GetErrorText ?? "Failed to request credential reset.", Severity.Error); + return; + } + + _resetCode = result.Value.Token; + _resetRequested = true; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor index 0a44b8b9..b78db16f 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor @@ -79,160 +79,3 @@ Close - -@code { - private MudDataGrid? _grid; - private bool _loading; - private string? _newRoleName; - - [CascadingParameter] - private IMudDialogInstance MudDialog { get; set; } = default!; - - [Parameter] - public UAuthState AuthState { get; set; } = default!; - - 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.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 CommittedItemChanges(RoleInfo role) - { - var req = new RenameRoleRequest - { - Name = role.Name - }; - - var result = await UAuthClient.Authorization.RenameRoleAsync(role.Id, req); - - if (result.IsSuccess) - { - Snackbar.Add("Role renamed", Severity.Success); - } - else - { - Snackbar.Add(result.Problem?.Title ?? "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.Problem?.Title ?? "Create 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(); - var result = await UAuthClient.Authorization.DeleteRoleAsync(roleId, 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.Problem?.Detail ?? result.Problem?.Title ?? "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(); - -} \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor.cs new file mode 100644 index 00000000..d77de014 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor.cs @@ -0,0 +1,163 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Dialogs; + +public partial class RoleDialog +{ + private MudDataGrid? _grid; + private bool _loading; + private string? _newRoleName; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + 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.GetErrorText ?? "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 + { + Name = role.Name + }; + + var result = await UAuthClient.Authorization.RenameRoleAsync(role.Id, req); + + if (result.IsSuccess) + { + Snackbar.Add("Role renamed", Severity.Success); + } + else + { + Snackbar.Add(result.GetErrorText ?? "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.GetErrorText ?? "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(); + var result = await UAuthClient.Authorization.DeleteRoleAsync(roleId, 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.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor index c51e9987..8ecf2a15 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor @@ -138,7 +138,7 @@ Revoke Other Devices } - Sessions @@ -148,7 +148,7 @@ - + @@ -206,7 +206,7 @@ - + } @@ -215,278 +215,3 @@ Cancel - -@code { - private MudDataGrid? _grid; - private bool _loading = false; - private bool _reloadQueued; - private SessionChainDetailDto? _chainDetail; - - [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) - { - 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.LogoutAllDevicesSelfAsync(); - } - else - { - result = await UAuthClient.Flows.LogoutAllDevicesAdminAsync(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.GetErrorText ?? "Failed to logout", Severity.Error); - } - } - - private async Task LogoutOthersAsync() - { - var result = await UAuthClient.Flows.LogoutOtherDevicesSelfAsync(); - - if (result.IsSuccess) - { - Snackbar.Add("Logged out of other devices.", Severity.Success); - } - else - { - Snackbar.Add(result?.GetErrorText ?? "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.LogoutDeviceSelfAsync(request); - } - else - { - result = await UAuthClient.Flows.LogoutDeviceAdminAsync(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.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "Failed to fetch chain details.", Severity.Error); - _chainDetail = null; - } - } - - private void ClearDetail() - { - _chainDetail = null; - } - - private void Cancel() => MudDialog.Cancel(); -} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor.cs new file mode 100644 index 00000000..bcf2bb77 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor.cs @@ -0,0 +1,284 @@ +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.UltimateAuth.Sample.BlazorServer.Components.Dialogs; + +public partial class SessionDialog +{ + private MudDataGrid? _grid; + private bool _loading = false; + private bool _reloadQueued; + private SessionChainDetail? _chainDetail; + + [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) + { + 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.LogoutAllDevicesSelfAsync(); + } + else + { + result = await UAuthClient.Flows.LogoutAllDevicesAdminAsync(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.GetErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task LogoutOthersAsync() + { + var result = await UAuthClient.Flows.LogoutOtherDevicesSelfAsync(); + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of other devices.", Severity.Success); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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.LogoutDeviceSelfAsync(request); + } + else + { + result = await UAuthClient.Flows.LogoutDeviceAdminAsync(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.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "Failed to fetch chain details.", Severity.Error); + _chainDetail = null; + } + } + + private void ClearDetail() + { + _chainDetail = null; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor index 92aeaa8e..4cd64ff5 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor @@ -73,93 +73,3 @@ Close - -@code { - private UserView? _user; - private UserStatus _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.GetProfileAsync(UserKey); - - if (result.IsSuccess) - { - _user = result.Value; - _status = _user?.Status ?? UserStatus.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.ChangeStatusAdminAsync(_user.UserKey, request); - - if (result.IsSuccess) - { - Snackbar.Add("User status updated", Severity.Success); - _user = _user with { Status = _status }; - } - else - { - Snackbar.Add(result.GetErrorText ?? "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(); - } -} \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor.cs new file mode 100644 index 00000000..f856566a --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/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.UltimateAuth.Sample.BlazorServer.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Dialogs; + +public partial class UserDetailDialog +{ + private UserView? _user; + private UserStatus _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.GetProfileAsync(UserKey); + + if (result.IsSuccess) + { + _user = result.Value; + _status = _user?.Status ?? UserStatus.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.ChangeStatusAdminAsync(_user.UserKey, request); + + if (result.IsSuccess) + { + Snackbar.Add("User status updated", Severity.Success); + _user = _user with { Status = _status }; + } + else + { + Snackbar.Add(result.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor index 9ae32dfa..6e754848 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor @@ -47,108 +47,3 @@ Close - -@code { - - [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 result = await UAuthClient.Authorization.AssignRoleToUserAsync(UserKey, _selectedRole); - - if (result.IsSuccess) - { - _roles.Add(_selectedRole); - Snackbar.Add("Role assigned", Severity.Success); - } - else - { - Snackbar.Add(result.GetErrorText ?? "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 result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(UserKey, role); - - if (result.IsSuccess) - { - _roles.Remove(role); - Snackbar.Add("Role removed", Severity.Success); - } - else - { - Snackbar.Add(result.GetErrorText ?? "Failed", Severity.Error); - } - } - - private void Close() => MudDialog.Close(); -} \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor.cs new file mode 100644 index 00000000..eb3557c4 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor.cs @@ -0,0 +1,112 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.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 result = await UAuthClient.Authorization.AssignRoleToUserAsync(UserKey, _selectedRole); + + if (result.IsSuccess) + { + _roles.Add(_selectedRole); + Snackbar.Add("Role assigned", Severity.Success); + } + else + { + Snackbar.Add(result.GetErrorText ?? "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 result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(UserKey, role); + + if (result.IsSuccess) + { + _roles.Remove(role); + Snackbar.Add("Role removed.", Severity.Success); + } + else + { + Snackbar.Add(result.GetErrorText ?? "Failed", Severity.Error); + } + } + + private void Close() => MudDialog.Close(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor index 39f3c5e1..bf19af4e 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor @@ -83,169 +83,3 @@ Close - -@code { - private MudDataGrid? _grid; - private bool _loading; - private string? _search; - private bool _reloadQueued; - private UserStatus? _statusFilter; - - [CascadingParameter] - IMudDialogInstance MudDialog { get; set; } = default!; - - [Parameter] - public UAuthState AuthState { get; set; } = default!; - - 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.QueryUsersAsync(req); - - if (!res.IsSuccess || res.Value == null) - { - Snackbar.Add(res.GetErrorText ?? "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.GetErrorText ?? "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(); - } -} \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor.cs new file mode 100644 index 00000000..ff112f29 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor.cs @@ -0,0 +1,176 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Sample.BlazorServer.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Dialogs; + +public partial class UsersDialog +{ + private MudDataGrid? _grid; + private bool _loading; + private string? _search; + private bool _reloadQueued; + private UserStatus? _statusFilter; + + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + 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.QueryUsersAsync(req); + + if (!res.IsSuccess || res.Value == null) + { + Snackbar.Add(res.GetErrorText ?? "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.GetErrorText ?? "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/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor index b059ee89..5dc5d8aa 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor @@ -1,2 +1,26 @@ @page "/authorized-test" -@attribute [Authorize] \ No newline at end of file +@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. + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor index 0518403b..b71e9282 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor @@ -11,6 +11,7 @@ @using CodeBeam.UltimateAuth.Core.Contracts @using CodeBeam.UltimateAuth.Core.Defaults @using CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Custom +@using Microsoft.AspNetCore.Authorization @if (AuthState?.Identity?.UserStatus == UserStatus.SelfSuspended) { @@ -29,6 +30,22 @@ return; } +@if (AuthState?.Identity?.UserStatus == UserStatus.Suspended) +{ + + + + Your account is suspended. Please contact with administrator. + + + + Logout + + + + return; +} + @@ -45,20 +62,19 @@ - Validate + Validate - Manual Refresh + Manual Refresh - - Logout + + Logout @@ -69,23 +85,23 @@ - Manage Sessions + Manage Sessions - Manage Profile + Manage Profile - Manage Identifiers + Manage Identifiers - Manage Credentials + Manage Credentials - Suspend | Delete Account + Suspend | Delete Account @@ -110,16 +126,18 @@ - User Management + @* *@ + @* *@ + User Management + @* *@ - - - - Role Management - + + @* *@ + Role Management + @* *@ @@ -398,8 +416,8 @@ - Touched - @Diagnostics.RefreshTouchedCount + Touched/Rotated + @Diagnostics.RefreshTouchedCount / @Diagnostics.RefreshRotatedCount diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs index 72aab311..844f6483 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs @@ -177,7 +177,7 @@ private async Task OpenCredentialDialog() private async Task OpenAccountStatusDialog() { - await DialogService.ShowAsync("Manage Account", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + await DialogService.ShowAsync("Manage Account", GetDialogParameters(), UAuthDialog.GetDialogOptions(MaxWidth.ExtraSmall)); } private async Task OpenUserDialog() @@ -213,18 +213,6 @@ private async Task SetAccountActiveAsync() } } - private string? _roles = "Admin"; - private void RefreshHiddenState() - { - if (_roles == "Admin") - { - _roles = "User"; - return; - } - - _roles = "Admin"; - } - public override void Dispose() { base.Dispose(); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor index 2c0e9b77..d8eb7138 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor @@ -1,7 +1,7 @@ @inject NavigationManager Nav - + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs index 82645070..9a193aa8 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs @@ -39,7 +39,7 @@ private async Task HandleRegisterAsync() } else { - Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Failed to create user.", Severity.Error); + Snackbar.Add(result.GetErrorText ?? "Failed to create user.", Severity.Error); } } } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor index c9f4b753..03c4e497 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Routes.razor @@ -3,8 +3,7 @@ @inject ISnackbar Snackbar @inject DarkModeManager DarkModeManager - - + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor index b5d250a3..7d8ad8a5 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/App.razor @@ -1,29 +1,59 @@ -@inject ISnackbar Snackbar +@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Infrastructure +@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Pages +@inject ISnackbar Snackbar +@inject DarkModeManager DarkModeManager + + + + + - - - - - - - - - - - - Not found - -

Sorry, there's nothing at this address.

-
-
-
+ + + + + + + + + + +
@code { - private void HandleReauth() + private async Task HandleReauth() { Snackbar.Add("Reauthentication required. Please log in again.", Severity.Warning); } -} \ No newline at end of file + + #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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogo.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogo.razor new file mode 100644 index 00000000..2806b7d3 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogo.razor @@ -0,0 +1,19 @@ +@namespace CodeBeam.UltimateAuth.Sample +@inherits ComponentBase + + + + @if (Variant == UAuthLogoVariant.Brand) + { + + + + } + else + { + + + + } + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogo.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogo.razor.cs new file mode 100644 index 00000000..030d9b66 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogoVariant.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogoVariant.cs new file mode 100644 index 00000000..fe3be220 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Brand/UAuthLogoVariant.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample; + +public enum UAuthLogoVariant +{ + Brand, + Mono +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Common/UAuthDialog.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Common/UAuthDialog.cs new file mode 100644 index 00000000..5e52a199 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Common/UAuthDialog.cs @@ -0,0 +1,29 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Custom/UAuthPageComponent.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Custom/UAuthPageComponent.razor new file mode 100644 index 00000000..5af543e4 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Custom/UAuthPageComponent.razor @@ -0,0 +1,10 @@ + + + @ChildContent + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/AccountStatusDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/AccountStatusDialog.razor new file mode 100644 index 00000000..0c91e45c --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/AccountStatusDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/AccountStatusDialog.razor.cs new file mode 100644 index 00000000..eef2e773 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.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 = SelfUserStatus.SelfSuspended }; + var result = await UAuthClient.Users.ChangeStatusSelfAsync(request); + if (result.IsSuccess) + { + Snackbar.Add("Your account suspended successfully.", Severity.Success); + MudDialog.Close(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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?.GetErrorText ?? "Delete failed.", Severity.Error); + } + } +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor new file mode 100644 index 00000000..9a514935 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor.cs new file mode 100644 index 00000000..ccff3139 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CreateUserDialog.razor.cs @@ -0,0 +1,55 @@ +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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.Validate(); + + 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.CreateAdminAsync(request); + + if (!result.IsSuccess) + { + Snackbar.Add(result.GetErrorText ?? "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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor new file mode 100644 index 00000000..660b7c3a --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/CredentialDialog.razor.cs new file mode 100644 index 00000000..c26bb246 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.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.Validate(); + 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.ChangeCredentialAsync(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.GetErrorText ?? "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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/IdentifierDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/IdentifierDialog.razor new file mode 100644 index 00000000..24c9e8c9 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/IdentifierDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/IdentifierDialog.razor.cs new file mode 100644 index 00000000..7838af7f --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.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.GetMyIdentifiersAsync(); + 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.GetMyIdentifiersAsync(req); + } + else + { + res = await UAuthClient.Identifiers.GetUserIdentifiersAsync(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.UpdateSelfAsync(updateRequest); + } + else + { + result = await UAuthClient.Identifiers.UpdateAdminAsync(UserKey.Value, updateRequest); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier updated successfully", Severity.Success); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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.AddSelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.AddAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier added successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.VerifySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.VerifyAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier verified successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to verify primary identifier", Severity.Error); + } + } + + private async Task SetPrimaryAsync(Guid id) + { + SetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.SetPrimarySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.SetPrimaryAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier set successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to set primary identifier", Severity.Error); + } + } + + private async Task UnsetPrimaryAsync(Guid id) + { + UnsetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.UnsetPrimarySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.UnsetPrimaryAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier unset successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to unset primary identifier", Severity.Error); + } + } + + private async Task DeleteIdentifier(Guid id) + { + DeleteUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.DeleteSelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.DeleteAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier deleted successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "Failed to delete identifier", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/PermissionDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/PermissionDialog.razor new file mode 100644 index 00000000..8e0df863 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/PermissionDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/PermissionDialog.razor.cs new file mode 100644 index 00000000..214c690b --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/PermissionDialog.razor.cs @@ -0,0 +1,119 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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 SetPermissionsRequest + { + Permissions = permissions + }; + + var result = await UAuthClient.Authorization.SetPermissionsAsync(Role.Id, req); + + if (!result.IsSuccess) + { + Snackbar.Add(result.GetErrorText ?? "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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor new file mode 100644 index 00000000..a36af169 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ProfileDialog.razor.cs new file mode 100644 index 00000000..ccd61162 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.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.GetProfileAsync(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.Validate(); + 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.UpdateProfileAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Profile updated", Severity.Success); + MudDialog.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add(result.GetErrorText ?? "Failed to update profile", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ResetDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ResetDialog.razor new file mode 100644 index 00000000..06a515aa --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ResetDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/ResetDialog.razor.cs new file mode 100644 index 00000000..5d6c99a7 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.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 BeginCredentialResetRequest + { + 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.GetErrorText ?? "Failed to request credential reset.", Severity.Error); + return; + } + + _resetCode = result.Value.Token; + _resetRequested = true; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/RoleDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/RoleDialog.razor new file mode 100644 index 00000000..bfcf9428 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/RoleDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/RoleDialog.razor.cs new file mode 100644 index 00000000..298c87b0 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/RoleDialog.razor.cs @@ -0,0 +1,175 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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.GetErrorText ?? "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 + { + Name = role.Name + }; + + var result = await UAuthClient.Authorization.RenameRoleAsync(role.Id, req); + + if (result.IsSuccess) + { + Snackbar.Add("Role renamed", Severity.Success); + } + else + { + Snackbar.Add(result.GetErrorText ?? "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.GetErrorText ?? "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(); + var result = await UAuthClient.Authorization.DeleteRoleAsync(roleId, 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.GetErrorText ?? "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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/SessionDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/SessionDialog.razor new file mode 100644 index 00000000..e5ee0c4d --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/SessionDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/SessionDialog.razor.cs new file mode 100644 index 00000000..3793677a --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.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.LogoutAllDevicesSelfAsync(); + } + else + { + result = await UAuthClient.Flows.LogoutAllDevicesAdminAsync(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.GetErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task LogoutOthersAsync() + { + var result = await UAuthClient.Flows.LogoutOtherDevicesSelfAsync(); + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of other devices.", Severity.Success); + } + else + { + Snackbar.Add(result?.GetErrorText ?? "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.LogoutDeviceSelfAsync(request); + } + else + { + result = await UAuthClient.Flows.LogoutDeviceAdminAsync(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.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "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?.GetErrorText ?? "Failed to fetch chain details.", Severity.Error); + _chainDetail = null; + } + } + + private void ClearDetail() + { + _chainDetail = null; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserDetailDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserDetailDialog.razor new file mode 100644 index 00000000..3a950e53 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserDetailDialog.razor @@ -0,0 +1,75 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserDetailDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserDetailDialog.razor.cs new file mode 100644 index 00000000..d5046440 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Components.Dialogs; + +public partial class UserDetailDialog +{ + private UserView? _user; + private UserStatus _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.GetProfileAsync(UserKey); + + if (result.IsSuccess) + { + _user = result.Value; + _status = _user?.Status ?? UserStatus.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.ChangeStatusAdminAsync(_user.UserKey, request); + + if (result.IsSuccess) + { + Snackbar.Add("User status updated", Severity.Success); + _user = _user with { Status = _status }; + } + else + { + Snackbar.Add(result.GetErrorText ?? "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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserRoleDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserRoleDialog.razor new file mode 100644 index 00000000..6e754848 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserRoleDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserRoleDialog.razor.cs new file mode 100644 index 00000000..a3b2fa0c --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UserRoleDialog.razor.cs @@ -0,0 +1,112 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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 result = await UAuthClient.Authorization.AssignRoleToUserAsync(UserKey, _selectedRole); + + if (result.IsSuccess) + { + _roles.Add(_selectedRole); + Snackbar.Add("Role assigned", Severity.Success); + } + else + { + Snackbar.Add(result.GetErrorText ?? "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 result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(UserKey, role); + + if (result.IsSuccess) + { + _roles.Remove(role); + Snackbar.Add("Role removed.", Severity.Success); + } + else + { + Snackbar.Add(result.GetErrorText ?? "Failed", Severity.Error); + } + } + + private void Close() => MudDialog.Close(); +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UsersDialog.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UsersDialog.razor new file mode 100644 index 00000000..f9df9a84 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UsersDialog.razor @@ -0,0 +1,94 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UsersDialog.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Components/Dialogs/UsersDialog.razor.cs new file mode 100644 index 00000000..64dcbf6c --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.UltimateAuth.Sample.BlazorStandaloneWasm.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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.QueryUsersAsync(req); + + if (!res.IsSuccess || res.Value == null) + { + Snackbar.Add(res.GetErrorText ?? "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.GetErrorText ?? "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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Infrastructure/DarkModeManager.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Infrastructure/DarkModeManager.cs new file mode 100644 index 00000000..bd8900e4 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Infrastructure/DarkModeManager.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Infrastructure; + +public sealed class DarkModeManager +{ + private const string StorageKey = "uauth:theme:dark"; + + private readonly IBrowserStorage _storage; + + public DarkModeManager(IBrowserStorage 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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor index f451950a..2788afc2 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor @@ -1,78 +1,55 @@ @inherits LayoutComponentBase @inject IUAuthClient UAuthClient @inject ISnackbar Snackbar +@inject NavigationManager Nav - - UltimateAuth - Blazor Server Sample + + + UltimateAuth + + Blazor WASM Sample - - - - - - - - Text - - - - - - - - - - - - @(state.Identity?.PrimaryUserName?.Substring(0, 1).ToUpper()) - - - - - - - - - - - - - - @state.Identity?.PrimaryUserName - + + + + + + +
+ + + @((state.Identity?.DisplayName ?? "?").Trim() is var n ? (n.Length >= 2 ? n[..2] : n[..1]) : "?") + + +
+
+ + + @state.Identity?.DisplayName + @string.Join(", ", state.Claims.Roles) - - - Refresh Session - - - - - Logout - -
- - - - Reauthenticate - - - - - - Sign In - - -
-
- -
+ + + + + @if (state.Identity?.SessionState is not null && state.Identity.SessionState != SessionState.Active) + { + + + } + + + + + + + +
@@ -80,17 +57,9 @@
-@code { - [CascadingParameter] - public UAuthState UAuth { get; set; } = default!; - - private async Task Refresh() - { - await UAuthClient.Flows.RefreshAsync(); - } - private async Task Logout() - { - await UAuthClient.Flows.LogoutAsync(); - } -} +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor.cs new file mode 100644 index 00000000..8567fb06 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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.BlazorStandaloneWasm.Infrastructure; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor.css b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor.css deleted file mode 100644 index ecf25e5b..00000000 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Layout/MainLayout.razor.css +++ /dev/null @@ -1,77 +0,0 @@ -.page { - position: relative; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; - display: flex; - align-items: center; -} - - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } - - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } - - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } - -@media (max-width: 640.98px) { - .top-row { - justify-content: space-between; - } - - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } -} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AnonymousTestPage.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AnonymousTestPage.razor new file mode 100644 index 00000000..10d035ba --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AnonymousTestPage.razor @@ -0,0 +1 @@ +@page "/anonymous-test" diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AuthorizedTestPage.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/AuthorizedTestPage.razor new file mode 100644 index 00000000..e5554c4e --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor index bcda64ca..984e3912 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor @@ -1,145 +1,444 @@ -@page "/" -@page "/login" -@using CodeBeam.UltimateAuth.Client.Authentication -@using CodeBeam.UltimateAuth.Client.Device -@using CodeBeam.UltimateAuth.Client.Diagnostics -@using CodeBeam.UltimateAuth.Client.Infrastructure -@using CodeBeam.UltimateAuth.Client.Runtime -@using CodeBeam.UltimateAuth.Core.Abstractions -@using CodeBeam.UltimateAuth.Core.Runtime -@inject IUAuthStateManager StateManager -@inject IUAuthProductInfoProvider ProductInfo -@inject ISnackbar Snackbar +@page "/home" +@attribute [Authorize] +@inherits UAuthFlowPageBase + @inject IUAuthClient UAuthClient -@inject NavigationManager Nav -@inject IUAuthClientProductInfoProvider ClientProductInfo -@inject AuthenticationStateProvider AuthStateProvider @inject UAuthClientDiagnostics Diagnostics -@inject IUAuthClientBootstrapper Bootstrapper -@inject IDeviceIdProvider DeviceIdProvider - -
- - - - Welcome to UltimateAuth! - - - Login - - - - - Validate - Logout - Refresh - - - - Programmatic Login - Start Pkce Login - +@inject AuthenticationStateProvider AuthStateProvider +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@using System.Security.Claims +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Core.Defaults +@using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Components.Custom +@using Microsoft.AspNetCore.Authorization - - @ClientProductInfo.Get().ProductName v @ClientProductInfo.Get().Version - Client Profile: @ClientProductInfo.Get().ClientProfile.ToString() - +@if (AuthState?.Identity?.UserStatus == UserStatus.SelfSuspended) +{ + + + + Your account is suspended. Please active it before continue. + - - StateHasChanged - Refresh Auth State - State of Authentication: - From UltimateAuth: @(Auth?.IsAuthenticated == true ? "Authenticated" : "Not Authenticated") - UserId:@(Auth?.Identity?.UserKey) - From ASPNET Core: @(_authState?.User?.Identity?.IsAuthenticated == true ? "Authenticated" : "Not Authenticated") - UserId:@(_authState?.User?.Identity?.Name) - - - Authorized context is shown. - - - Not Authorized context is shown. - - - - - This is Admin content. - + + Set Active + Logout + + + return; +} +@if (AuthState?.Identity?.UserStatus == UserStatus.Suspended) +{ + - - UltimateAuth Client Diagnostics - - - - Started: @Diagnostics.StartCount - @Diagnostics.StartedAt - Stopped: @Diagnostics.StopCount - @Diagnostics.StoppedAt - Terminated: @Diagnostics.TerminatedCount - @Diagnostics.TerminatedAt (@Diagnostics.TerminationReason.ToString()) - - - - Refresh Attempts: @Diagnostics.RefreshAttemptCount - Auto: @Diagnostics.AutomaticRefreshCount - Manual: @Diagnostics.ManualRefreshCount - - - Touched Success: @Diagnostics.RefreshTouchedCount - - - No-Op Success: @Diagnostics.RefreshNoOpCount - - - ReauthRequired: @Diagnostics.RefreshReauthRequiredCount - - - Unknown: @Diagnostics.RefreshSuccessCount - - + + Your account is suspended. Please contact with administrator. + + + + Logout + - - -
- -Ping UAuthHub -Ping ResourceApi - -@code { - // private string? _result; - // private Severity _severity = Severity.Info; - - private async Task CallHub() - { - // try - // { - // var client = HttpClientFactory.CreateClient("UAuthHub"); - // var response = await client.GetStringAsync("/health"); - - // _result = $"UAuthHub response: {response}"; - // _severity = Severity.Success; - // } - // catch (Exception ex) - // { - // _result = $"UAuthHub error: {ex.Message}"; - // _severity = Severity.Error; - // } - // Snackbar.Add(_result, _severity); - } - - private async Task CallApi() - { - // try - // { - // var client = HttpClientFactory.CreateClient("ResourceApi"); - // var response = await client.GetStringAsync("/health"); - - // _result = $"ResourceApi response: {response}"; - // _severity = Severity.Success; - // } - // catch (Exception ex) - // { - // _result = $"ResourceApi error: {ex.Message}"; - // _severity = Severity.Error; - // } - // Snackbar.Add(_result, _severity); - } + + 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 + @* *@ + + + + } + + + + + + + + + + + @((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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs index 6ecdf083..0a44f4fd 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs @@ -1,125 +1,222 @@ using CodeBeam.UltimateAuth.Client; -using CodeBeam.UltimateAuth.Client.Authentication; +using CodeBeam.UltimateAuth.Client.Errors; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Components; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Common; +using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Components.Dialogs; +using CodeBeam.UltimateAuth.Users.Contracts; using Microsoft.AspNetCore.Components.Authorization; using MudBlazor; +using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Pages +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Pages; + +public partial class Home : UAuthFlowPageBase { - public partial class Home - { - [CascadingParameter] - public UAuthState Auth { get; set; } = null!; + private string _selectedAuthState = "UAuthState"; + private ClaimsPrincipal? _aspNetCoreState; - private string? _username; - private string? _password; + private bool _showAdminPreview = false; - private AuthenticationState _authState = null!; + protected override async Task OnInitializedAsync() + { + var initial = await AuthStateProvider.GetAuthenticationStateAsync(); + _aspNetCoreState = initial.User; + AuthStateProvider.AuthenticationStateChanged += OnAuthStateChanged; + Diagnostics.Changed += OnDiagnosticsChanged; + } - protected override async Task OnInitializedAsync() - { - Diagnostics.Changed += OnDiagnosticsChanged; - //_authState = await AuthStateProvider.GetAuthenticationStateAsync(); - } + private void OnAuthStateChanged(Task task) + { + _ = HandleAuthStateChangedAsync(task); + } - protected override async Task OnAfterRenderAsync(bool firstRender) + private async Task HandleAuthStateChangedAsync(Task task) + { + try { - if (firstRender) - { - await StateManager.EnsureAsync(); - _authState = await AuthStateProvider.GetAuthenticationStateAsync(); - StateHasChanged(); - } + var state = await task; + _aspNetCoreState = state.User; + await InvokeAsync(StateHasChanged); } - - private void OnDiagnosticsChanged() + catch { - InvokeAsync(StateHasChanged); - } - private async Task ProgrammaticLogin() - { - var deviceId = await DeviceIdProvider.GetOrCreateAsync(); - var request = new LoginRequest - { - Identifier = "admin", - Secret = "admin", - }; - await UAuthClient.Flows.LoginAsync(request); } + } - private async Task StartPkceLogin() - { - await UAuthClient.Flows.BeginPkceAsync(); - //await UAuthClient.NavigateToHubLoginAsync(Nav.Uri); - } + private void OnDiagnosticsChanged() + { + InvokeAsync(StateHasChanged); + } + + private async Task Logout() => await UAuthClient.Flows.LogoutAsync(); - private async Task ValidateAsync() + private async Task RefreshSession() => await UAuthClient.Flows.RefreshAsync(false); + + private async Task Validate() + { + try { var result = await UAuthClient.Flows.ValidateAsync(); - Snackbar.Add( - result.IsValid ? "Session is valid ✅" : $"Session invalid ❌ ({result.State})", - result.IsValid ? Severity.Success : Severity.Error); - } + 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; - private async Task LogoutAsync() + 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) { - await UAuthClient.Flows.LogoutAsync(); - Snackbar.Add("Logged out", Severity.Success); + Snackbar.Add("Network error.", Severity.Error); } - - private async Task RefreshAsync() + catch (UAuthProtocolException) { - await UAuthClient.Flows.RefreshAsync(); + Snackbar.Add("Invalid response.", Severity.Error); } - - private async Task RefreshAuthState() + catch (UAuthException ex) { - await StateManager.OnLoginAsync(); + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); } - - protected override void OnAfterRender(bool firstRender) + catch (Exception ex) { - if (firstRender) - { - var uri = Nav.ToAbsoluteUri(Nav.Uri); - var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query); - - if (query.TryGetValue("error", out var error)) - { - ShowLoginError(error.ToString()); - ClearQueryString(); - } - } + 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 void ShowLoginError(string code) + 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 DialogParameters GetDialogParameters() + { + return new DialogParameters { - var message = code switch - { - "invalid" => "Invalid username or password.", - "locked" => "Your account is locked.", - "mfa" => "Multi-factor authentication required.", - _ => "Login failed." - }; + ["AuthState"] = AuthState + }; + } - Snackbar.Add(message, Severity.Error); - } + private async Task SetAccountActiveAsync() + { + ChangeUserStatusSelfRequest request = new() { NewStatus = SelfUserStatus.Active }; + var result = await UAuthClient.Users.ChangeStatusSelfAsync(request); - private void ClearQueryString() + if (result.IsSuccess) { - var uri = new Uri(Nav.Uri); - var clean = uri.GetLeftPart(UriPartial.Path); - Nav.NavigateTo(clean, replace: true); + Snackbar.Add("Account activated successfully.", Severity.Success); } - - public void Dispose() + else { - Diagnostics.Changed -= OnDiagnosticsChanged; + 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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/LandingPage.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/LandingPage.razor new file mode 100644 index 00000000..1e4a9016 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/LandingPage.razor @@ -0,0 +1,4 @@ +@page "/" + +@inject NavigationManager Nav +@inject AuthenticationStateProvider AuthProvider diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/LandingPage.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/LandingPage.razor.cs new file mode 100644 index 00000000..e5844e6d --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/LandingPage.razor.cs @@ -0,0 +1,17 @@ +using CodeBeam.UltimateAuth.Core.Defaults; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor new file mode 100644 index 00000000..c82f0e04 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor @@ -0,0 +1,134 @@ +@page "/login" +@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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor.cs new file mode 100644 index 00000000..015076b6 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Login.razor.cs @@ -0,0 +1,200 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Components.Dialogs; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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 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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/NotAuthorized.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/NotAuthorized.razor new file mode 100644 index 00000000..d8eb7138 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/NotAuthorized.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/NotAuthorized.razor.cs new file mode 100644 index 00000000..f46ca21f --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/NotAuthorized.razor.cs @@ -0,0 +1,15 @@ +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor new file mode 100644 index 00000000..e32cc79c --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor @@ -0,0 +1,54 @@ +@page "/register" +@inherits UAuthFlowPageBase + +@implements IDisposable +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IUAuthClientProductInfoProvider ClientProductInfoProvider +@inject IDeviceIdProvider DeviceIdProvider +@inject IDialogService DialogService + + + + + + + + + + + + + + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor.cs new file mode 100644 index 00000000..3486ae47 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Users.Contracts; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.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.Validate(); + + 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 succesfully.", Severity.Success); + } + else + { + Snackbar.Add(result.GetErrorText ?? "Failed to create user.", Severity.Error); + } + } +} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor new file mode 100644 index 00000000..753878b8 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor.cs new file mode 100644 index 00000000..ee8b4919 --- /dev/null +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/ResetCredential.razor.cs @@ -0,0 +1,49 @@ +using CodeBeam.UltimateAuth.Credentials.Contracts; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Pages; + +public partial class ResetCredential +{ + private MudForm _form = null!; + private string? _code; + private string? _newPassword; + private string? _newPasswordCheck; + + private async Task ResetPasswordAsync() + { + await _form.Validate(); + 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 CompleteCredentialResetRequest + { + 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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Weather.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Weather.razor deleted file mode 100644 index f2defcf2..00000000 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Weather.razor +++ /dev/null @@ -1,57 +0,0 @@ -@page "/weather" -@inject HttpClient Http - -Weather - -

Weather

- -

This component demonstrates fetching data from the server.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await Http.GetFromJsonAsync("sample-data/weather.json"); - } - - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public string? Summary { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } -} diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs index 51de6048..0412ee70 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs @@ -1,6 +1,8 @@ using CodeBeam.UltimateAuth.Client.Extensions; +using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm; +using CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Infrastructure; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using MudBlazor.Services; @@ -15,17 +17,19 @@ builder.Services.AddUltimateAuth(); builder.Services.AddUltimateAuthClient(o => { - o.Endpoints.BasePath = "https://localhost:6110"; + o.Endpoints.BasePath = "https://localhost:6110/auth"; + o.Reauth.Behavior = ReauthBehavior.RaiseEvent; + o.Login.AllowCredentialPost = true; + o.Pkce.ReturnUrl = "https://localhost:6130/home"; }); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); - -builder.Services.AddAuthorizationCore(); - -builder.Services.AddMudServices(); +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); builder.Services.AddMudExtensions(); +builder.Services.AddScoped(); + //builder.Services.AddHttpClient("UAuthHub", client => //{ // client.BaseAddress = new Uri("https://localhost:6110"); diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor index cdb34a97..7fe2bd88 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/_Imports.razor @@ -2,6 +2,7 @@ @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @@ -10,8 +11,11 @@ @using UltimateAuth.Sample.BlazorStandaloneWasm @using UltimateAuth.Sample.BlazorStandaloneWasm.Layout -@using CodeBeam.UltimateAuth.Core +@using CodeBeam.UltimateAuth.Core.Abstractions +@using CodeBeam.UltimateAuth.Core.Domain @using CodeBeam.UltimateAuth.Client +@using CodeBeam.UltimateAuth.Client.Runtime +@using CodeBeam.UltimateAuth.Client.Diagnostics @using MudBlazor @using MudExtensions diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/UltimateAuth-Logo.png b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/UltimateAuth-Logo.png new file mode 100644 index 00000000..5b7282f1 Binary files /dev/null and b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/UltimateAuth-Logo.png differ diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/css/app.css b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/css/app.css index 7b3eb5d9..897c71bd 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/css/app.css +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/css/app.css @@ -111,4 +111,96 @@ code { .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { text-align: start; -} \ No newline at end of file +} + +.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/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/favicon.png b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/favicon.png deleted file mode 100644 index 8422b596..00000000 Binary files a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/favicon.png and /dev/null differ diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/icon-192.png b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/icon-192.png deleted file mode 100644 index 166f56da..00000000 Binary files a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/icon-192.png and /dev/null differ diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html index d0fc7487..879067ed 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/index.html @@ -7,7 +7,7 @@ UltimateAuth.Sample.BlazorStandaloneWasm - + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/sample-data/weather.json b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/sample-data/weather.json deleted file mode 100644 index b7459733..00000000 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/wwwroot/sample-data/weather.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "date": "2022-01-06", - "temperatureC": 1, - "summary": "Freezing" - }, - { - "date": "2022-01-07", - "temperatureC": 14, - "summary": "Bracing" - }, - { - "date": "2022-01-08", - "temperatureC": -13, - "summary": "Freezing" - }, - { - "date": "2022-01-09", - "temperatureC": -16, - "summary": "Balmy" - }, - { - "date": "2022-01-10", - "temperatureC": -2, - "summary": "Chilly" - } -] diff --git a/src/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs b/src/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs index 927cb991..eb21d03a 100644 --- a/src/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs +++ b/src/CodeBeam.UltimateAuth.Client/Diagnostics/UAuthClientDiagnostics.cs @@ -27,6 +27,7 @@ public sealed class UAuthClientDiagnostics public int AutomaticRefreshCount { get; private set; } public int RefreshTouchedCount { get; private set; } + public int RefreshRotatedCount { get; private set; } public int RefreshNoOpCount { get; private set; } public int RefreshReauthRequiredCount { get; private set; } public int RefreshSuccessCount { get; private set; } @@ -75,6 +76,12 @@ internal void MarkRefreshTouched() Changed?.Invoke(); } + internal void MarkRefreshRotated() + { + RefreshRotatedCount++; + Changed?.Invoke(); + } + internal void MarkRefreshNoOp() { RefreshNoOpCount++; diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs index 9223b29b..19cb5c77 100644 --- a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs @@ -99,8 +99,7 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol services.TryAddScoped(); services.TryAddScoped(); - //services.AddScoped(); - //services.AddScoped>(sp => sp.GetRequiredService()); + services.AddAuthorizationCore(); return services; } diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs index 6052ba7c..16fa248c 100644 --- a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs @@ -9,17 +9,17 @@ public sealed class UAuthClientPkceLoginFlowOptions /// public bool Enabled { get; set; } = true; - public string? ReturnUrl { get; init; } + public string? ReturnUrl { get; set; } /// /// Called after authorization_code is issued, /// before redirecting to the Hub. /// - public Func? OnAuthorized { get; init; } + public Func? OnAuthorized { get; set; } /// /// If false, BeginPkceAsync will NOT redirect automatically. /// Caller is responsible for navigation. /// - public bool AutoRedirect { get; init; } = true; + public bool AutoRedirect { get; set; } = true; } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs index d4812565..1ecb6eb4 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs @@ -5,15 +5,15 @@ namespace CodeBeam.UltimateAuth.Client.Services; public interface ISessionClient { - Task>> GetMyChainsAsync(PageRequest? request = null); - Task> GetMyChainDetailAsync(SessionChainId chainId); + Task>> GetMyChainsAsync(PageRequest? request = null); + Task> GetMyChainDetailAsync(SessionChainId chainId); Task> RevokeMyChainAsync(SessionChainId chainId); Task RevokeMyOtherChainsAsync(); Task RevokeAllMyChainsAsync(); - Task>> GetUserChainsAsync(UserKey userKey, PageRequest? request = null); - Task> GetUserChainDetailAsync(UserKey userKey, SessionChainId chainId); + Task>> GetUserChainsAsync(UserKey userKey, PageRequest? request = null); + Task> GetUserChainDetailAsync(UserKey userKey, SessionChainId chainId); Task RevokeUserSessionAsync(UserKey userKey, AuthSessionId sessionId); Task> RevokeUserChainAsync(UserKey userKey, SessionChainId chainId); Task RevokeUserRootAsync(UserKey userKey); diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs index cc2190e8..9b1ee616 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs @@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Client.Services; public interface IUserIdentifierClient { - Task>> GetMyIdentifiersAsync(PageRequest? request = null); + Task>> GetMyIdentifiersAsync(PageRequest? request = null); Task AddSelfAsync(AddUserIdentifierRequest request); Task UpdateSelfAsync(UpdateUserIdentifierRequest request); Task SetPrimarySelfAsync(SetPrimaryUserIdentifierRequest request); @@ -14,7 +14,7 @@ public interface IUserIdentifierClient Task VerifySelfAsync(VerifyUserIdentifierRequest request); Task DeleteSelfAsync(DeleteUserIdentifierRequest request); - Task>> GetUserIdentifiersAsync(UserKey userKey, PageRequest? request = null); + Task>> GetUserIdentifiersAsync(UserKey userKey, PageRequest? request = null); Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request); Task UpdateAdminAsync(UserKey userKey, UpdateUserIdentifierRequest request); Task SetPrimaryAdminAsync(UserKey userKey, SetPrimaryUserIdentifierRequest request); diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs index 4a04c194..a9e68bf4 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs @@ -22,14 +22,16 @@ internal class UAuthFlowClient : IFlowClient { private readonly IUAuthRequestClient _post; private readonly IUAuthClientEvents _events; + private readonly IDeviceIdProvider _deviceIdProvider; private readonly UAuthClientOptions _options; private readonly UAuthClientDiagnostics _diagnostics; private readonly NavigationManager _nav; - public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IOptions options, UAuthClientDiagnostics diagnostics, NavigationManager nav) + public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IDeviceIdProvider deviceIdProvider, IOptions options, UAuthClientDiagnostics diagnostics, NavigationManager nav) { _post = post; _events = events; + _deviceIdProvider = deviceIdProvider; _options = options.Value; _diagnostics = diagnostics; _nav = nav; @@ -101,6 +103,9 @@ public async Task RefreshAsync(bool isAuto = false) case RefreshOutcome.Touched: _diagnostics.MarkRefreshTouched(); break; + case RefreshOutcome.Rotated: + _diagnostics.MarkRefreshRotated(); + break; case RefreshOutcome.ReauthRequired: _diagnostics.MarkRefreshReauthRequired(); break; @@ -168,6 +173,7 @@ public async Task ValidateAsync() public async Task BeginPkceAsync(string? returnUrl = null) { var pkce = _options.Pkce; + var deviceId = await _deviceIdProvider.GetOrCreateAsync(); if (!pkce.Enabled) throw new InvalidOperationException("PKCE login is disabled by configuration."); @@ -182,7 +188,8 @@ public async Task BeginPkceAsync(string? returnUrl = null) new Dictionary { ["code_challenge"] = challenge, - ["challenge_method"] = "S256" + ["challenge_method"] = "S256", + ["device_id"] = deviceId.Value }); if (!raw.Ok || raw.Body is null) @@ -205,7 +212,7 @@ public async Task BeginPkceAsync(string? returnUrl = null) if (pkce.AutoRedirect) { - await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl); + await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl, deviceId.Value); } } @@ -229,7 +236,7 @@ public async Task CompletePkceLoginAsync(PkceLoginRequest request) ["return_url"] = request.ReturnUrl, ["Identifier"] = request.Identifier ?? string.Empty, - ["Secret"] = request.Secret ?? string.Empty + ["Secret"] = request.Secret ?? string.Empty, }; await _post.NavigateAsync(url, payload); @@ -289,7 +296,7 @@ public async Task LogoutAllDevicesAdminAsync(UserKey userKey) } - private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl) + private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl, string deviceId) { var hubLoginUrl = Url(_options.Endpoints.HubLoginPath); @@ -298,7 +305,8 @@ private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifi ["authorization_code"] = authorizationCode, ["code_verifier"] = codeVerifier, ["return_url"] = returnUrl, - ["client_profile"] = _options.ClientProfile.ToString() + ["client_profile"] = _options.ClientProfile.ToString(), + ["device_id"] = deviceId }; return _post.NavigateAsync(hubLoginUrl, data); diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs index b9740e7c..90a38e5b 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs @@ -24,17 +24,17 @@ private string Url(string path) => UAuthUrlBuilder.Build(_options.Endpoints.BasePath, path, _options.MultiTenant); - public async Task>> GetMyChainsAsync(PageRequest? request = null) + public async Task>> GetMyChainsAsync(PageRequest? request = null) { request ??= new PageRequest(); var raw = await _request.SendJsonAsync(Url("/me/sessions/chains"), request); - return UAuthResultMapper.FromJson>(raw); + return UAuthResultMapper.FromJson>(raw); } - public async Task> GetMyChainDetailAsync(SessionChainId chainId) + public async Task> GetMyChainDetailAsync(SessionChainId chainId) { var raw = await _request.SendFormAsync(Url($"/me/sessions/chains/{chainId}")); - return UAuthResultMapper.FromJson(raw); + return UAuthResultMapper.FromJson(raw); } public async Task> RevokeMyChainAsync(SessionChainId chainId) @@ -70,17 +70,17 @@ public async Task RevokeAllMyChainsAsync() } - public async Task>> GetUserChainsAsync(UserKey userKey, PageRequest? request = null) + public async Task>> GetUserChainsAsync(UserKey userKey, PageRequest? request = null) { request ??= new PageRequest(); var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/sessions/chains"), request); - return UAuthResultMapper.FromJson>(raw); + return UAuthResultMapper.FromJson>(raw); } - public async Task> GetUserChainDetailAsync(UserKey userKey, SessionChainId chainId) + public async Task> GetUserChainDetailAsync(UserKey userKey, SessionChainId chainId) { var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/sessions/chains/{chainId}")); - return UAuthResultMapper.FromJson(raw); + return UAuthResultMapper.FromJson(raw); } public async Task RevokeUserSessionAsync(UserKey userKey, AuthSessionId sessionId) diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs index 9f047dc3..15d6593f 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs @@ -23,11 +23,11 @@ public UAuthUserIdentifierClient(IUAuthRequestClient request, IUAuthClientEvents private string Url(string path) => UAuthUrlBuilder.Build(_options.Endpoints.BasePath, path, _options.MultiTenant); - public async Task>> GetMyIdentifiersAsync(PageRequest? request = null) + public async Task>> GetMyIdentifiersAsync(PageRequest? request = null) { request ??= new PageRequest(); var raw = await _request.SendJsonAsync(Url("/me/identifiers/get"), request); - return UAuthResultMapper.FromJson>(raw); + return UAuthResultMapper.FromJson>(raw); } public async Task AddSelfAsync(AddUserIdentifierRequest request) @@ -90,11 +90,11 @@ public async Task DeleteSelfAsync(DeleteUserIdentifierRequest reque return UAuthResultMapper.From(raw); } - public async Task>> GetUserIdentifiersAsync(UserKey userKey, PageRequest? request = null) + public async Task>> GetUserIdentifiersAsync(UserKey userKey, PageRequest? request = null) { request ??= new PageRequest(); var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey.Value}/identifiers/get"), request); - return UAuthResultMapper.FromJson>(raw); + return UAuthResultMapper.FromJson>(raw); } public async Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request) diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainDetailDto.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainDetail.cs similarity index 86% rename from src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainDetailDto.cs rename to src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainDetail.cs index edea1d4b..d8977be4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainDetailDto.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainDetail.cs @@ -2,7 +2,7 @@ namespace CodeBeam.UltimateAuth.Core.Contracts; -public sealed class SessionChainDetailDto +public sealed class SessionChainDetail { public SessionChainId ChainId { get; init; } @@ -24,5 +24,5 @@ public sealed class SessionChainDetailDto public AuthSessionId? ActiveSessionId { get; init; } - public IReadOnlyList Sessions { get; init; } = []; + public IReadOnlyList Sessions { get; init; } = []; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainSummaryDto.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainSummary.cs similarity index 94% rename from src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainSummaryDto.cs rename to src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainSummary.cs index 51debfc8..3782b0c6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainSummaryDto.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionChainSummary.cs @@ -2,7 +2,7 @@ namespace CodeBeam.UltimateAuth.Core.Contracts; -public sealed record SessionChainSummaryDto +public sealed record SessionChainSummary { public required SessionChainId ChainId { get; init; } public string? DeviceType { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionInfoDto.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionInfo.cs similarity index 84% rename from src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionInfoDto.cs rename to src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionInfo.cs index cbf966e1..a04eac65 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionInfoDto.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/SessionInfo.cs @@ -2,7 +2,7 @@ namespace CodeBeam.UltimateAuth.Core.Contracts; -public sealed record SessionInfoDto( +public sealed record SessionInfo( AuthSessionId SessionId, DateTimeOffset CreatedAt, DateTimeOffset ExpiresAt, diff --git a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs index faf1dc22..58fc735b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs @@ -118,8 +118,8 @@ public static class Authorization public static class Roles { - public const string ReadSelf = "authorization.roles.read.self"; - public const string ReadAdmin = "authorization.roles.read.admin"; + public const string GetSelf = "authorization.roles.get.self"; + public const string GetAdmin = "authorization.roles.get.admin"; public const string AssignAdmin = "authorization.roles.assign.admin"; public const string RemoveAdmin = "authorization.roles.remove.admin"; public const string CreateAdmin = "authorization.roles.create.admin"; diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs index c163f0fd..74abb271 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs @@ -1,8 +1,10 @@ -using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; namespace CodeBeam.UltimateAuth.Server.Auth; public sealed record AuthExecutionContext { public required UAuthClientProfile? EffectiveClientProfile { get; init; } + public DeviceContext? Device { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs index bbc26679..fdefb918 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs @@ -145,4 +145,26 @@ public async ValueTask RecreateWithClientProfileAsync(AuthFlowC returnUrlInfo ); } + + public ValueTask RecreateWithDeviceAsync(AuthFlowContext existing, DeviceContext device, CancellationToken ct = default) + { + var flowType = existing.FlowType; + var tenant = existing.Tenant; + + return ValueTask.FromResult(new AuthFlowContext( + flowType, + existing.ClientProfile, + existing.EffectiveMode, + device, + tenant, + existing.IsAuthenticated, + existing.UserKey, + existing.Session, + existing.OriginalOptions, + existing.EffectiveOptions, + existing.Response, + existing.PrimaryTokenKind, + existing.ReturnUrlInfo + )); + } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs index d1679c40..ecb7e738 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs @@ -8,4 +8,5 @@ public interface IAuthFlowContextFactory { ValueTask CreateAsync(HttpContext httpContext, AuthFlowType flowType, CancellationToken ct = default); ValueTask RecreateWithClientProfileAsync(AuthFlowContext existing, UAuthClientProfile overriddenProfile, CancellationToken ct = default); + ValueTask RecreateWithDeviceAsync(AuthFlowContext existing, DeviceContext device, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs index b5bbd9e3..419fbc16 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs @@ -48,6 +48,7 @@ public async Task AuthorizeAsync(HttpContext ctx) { var authContext = _authContext.Current; + // TODO: Make PKCE flow free if (authContext.FlowType != AuthFlowType.Login) return Results.BadRequest("PKCE is only supported for login flow."); @@ -67,7 +68,7 @@ public async Task AuthorizeAsync(HttpContext ctx) clientProfile: authContext.ClientProfile, tenant: authContext.Tenant, redirectUri: request.RedirectUri, - deviceId: string.Empty // TODO: Fix here with device binding + deviceId: request.DeviceId ); var expiresAt = _clock.UtcNow.AddSeconds(_options.Pkce.AuthorizationCodeLifetimeSeconds); @@ -114,7 +115,7 @@ public async Task CompleteAsync(HttpContext ctx) clientProfile: authContext.ClientProfile, tenant: authContext.Tenant, redirectUri: null, - deviceId: string.Empty), + deviceId: artifact.Context.DeviceId), _clock.UtcNow); if (!validation.Success) @@ -135,6 +136,7 @@ public async Task CompleteAsync(HttpContext ctx) var execution = new AuthExecutionContext { EffectiveClientProfile = artifact.Context.ClientProfile, + Device = DeviceContext.Create(DeviceId.Create(artifact.Context.DeviceId), null, null, null, null, null) }; var result = await _flow.LoginAsync(authContext, execution, loginRequest, ctx.RequestAborted); @@ -178,12 +180,14 @@ public async Task CompleteAsync(HttpContext ctx) var codeChallenge = form["code_challenge"].ToString(); var challengeMethod = form["challenge_method"].ToString(); var redirectUri = form["redirect_uri"].ToString(); + var deviceId = form["device_id"].ToString(); return new PkceAuthorizeRequest { CodeChallenge = codeChallenge, ChallengeMethod = challengeMethod, - RedirectUri = string.IsNullOrWhiteSpace(redirectUri) ? null : redirectUri + RedirectUri = string.IsNullOrWhiteSpace(redirectUri) ? null : redirectUri, + DeviceId = deviceId }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs index 0fc21b33..fd21d395 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs @@ -352,12 +352,12 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options selfAuthz.MapPost("/check", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) => await h.CheckAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); - if (Enabled(UAuthActions.Authorization.Roles.ReadSelf)) + if (Enabled(UAuthActions.Authorization.Roles.GetSelf)) selfAuthz.MapPost("/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) => await h.GetMyRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); - if (Enabled(UAuthActions.Authorization.Roles.ReadAdmin)) + if (Enabled(UAuthActions.Authorization.Roles.GetAdmin)) adminAuthz.MapPost("/users/{userKey}/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) => await h.GetUserRolesAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizeRequest.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizeRequest.cs index 50622f30..9b5d390b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizeRequest.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizeRequest.cs @@ -5,4 +5,5 @@ internal sealed class PkceAuthorizeRequest public string CodeChallenge { get; init; } = default!; public string ChallengeMethod { get; init; } = default!; public string? RedirectUri { get; init; } + public string? DeviceId { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs index 324d70ec..46aaaf6c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs @@ -24,7 +24,9 @@ public void Write(HttpContext context, RefreshOutcome outcome) { RefreshOutcome.NoOp => "no-op", RefreshOutcome.Touched => "touched", + RefreshOutcome.Rotated => "rotated", RefreshOutcome.ReauthRequired => "reauth-required", + RefreshOutcome.Success => "success", _ => "unknown" }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IIdentifierValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IIdentifierValidator.cs index 21243698..8054fa86 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IIdentifierValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IIdentifierValidator.cs @@ -5,5 +5,5 @@ namespace CodeBeam.UltimateAuth.Server.Infrastructure; public interface IIdentifierValidator { - Task ValidateAsync(AccessContext context, UserIdentifierDto identifier, CancellationToken ct = default); + Task ValidateAsync(AccessContext context, UserIdentifierInfo identifier, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IdentifierValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IdentifierValidator.cs index 3fb399b3..627c73e1 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IdentifierValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/IdentifierValidator.cs @@ -15,7 +15,7 @@ public IdentifierValidator(IOptions options) _options = options.Value.IdentifierValidation; } - public Task ValidateAsync(AccessContext context, UserIdentifierDto identifier, CancellationToken ct = default) + public Task ValidateAsync(AccessContext context, UserIdentifierInfo identifier, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs index b08b2150..fdbc5c37 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserCreateValidator.cs @@ -25,7 +25,7 @@ public async Task ValidateAsync(AccessContext context if (!string.IsNullOrWhiteSpace(request.UserName)) { - var r = await _identifierValidator.ValidateAsync(context, new UserIdentifierDto() + var r = await _identifierValidator.ValidateAsync(context, new UserIdentifierInfo() { Type = UserIdentifierType.Username, Value = request.UserName @@ -36,7 +36,7 @@ public async Task ValidateAsync(AccessContext context if (!string.IsNullOrWhiteSpace(request.Email)) { - var r = await _identifierValidator.ValidateAsync(context, new UserIdentifierDto() + var r = await _identifierValidator.ValidateAsync(context, new UserIdentifierInfo() { Type = UserIdentifierType.Email, Value = request.Email @@ -47,7 +47,7 @@ public async Task ValidateAsync(AccessContext context if (!string.IsNullOrWhiteSpace(request.Phone)) { - var r = await _identifierValidator.ValidateAsync(context, new UserIdentifierDto() + var r = await _identifierValidator.ValidateAsync(context, new UserIdentifierInfo() { Type = UserIdentifierType.Phone, Value = request.Phone diff --git a/src/CodeBeam.UltimateAuth.Server/Services/ISessionApplicationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionApplicationService.cs index 3078e595..6d53040f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/ISessionApplicationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionApplicationService.cs @@ -5,9 +5,9 @@ namespace CodeBeam.UltimateAuth.Server.Services; public interface ISessionApplicationService { - Task> GetUserChainsAsync(AccessContext context,UserKey userKey, PageRequest request, CancellationToken ct = default); + Task> GetUserChainsAsync(AccessContext context,UserKey userKey, PageRequest request, CancellationToken ct = default); - Task GetUserChainDetailAsync(AccessContext context, UserKey userKey, SessionChainId chainId, CancellationToken ct = default); + Task GetUserChainDetailAsync(AccessContext context, UserKey userKey, SessionChainId chainId, CancellationToken ct = default); Task RevokeUserSessionAsync(AccessContext context, UserKey userKey, AuthSessionId sessionId, CancellationToken ct = default); diff --git a/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs index 8e6d6ad9..4b62d6ab 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/SessionApplicationService.cs @@ -19,9 +19,9 @@ public SessionApplicationService(IAccessOrchestrator accessOrchestrator, ISessio _clock = clock; } - public async Task> GetUserChainsAsync(AccessContext context, UserKey userKey, PageRequest request, CancellationToken ct = default) + public async Task> GetUserChainsAsync(AccessContext context, UserKey userKey, PageRequest request, CancellationToken ct = default) { - var command = new AccessCommand>(async innerCt => + var command = new AccessCommand>(async innerCt => { var store = _storeFactory.Create(context.ResourceTenant); request = request.Normalize(); @@ -32,43 +32,43 @@ public async Task> GetUserChainsAsync(Access { chains = request.SortBy switch { - nameof(SessionChainSummaryDto.ChainId) => request.Descending + nameof(SessionChainSummary.ChainId) => request.Descending ? chains.OrderByDescending(x => x.ChainId).ToList() : chains.OrderBy(x => x.Version).ToList(), - nameof(SessionChainSummaryDto.CreatedAt) => request.Descending + nameof(SessionChainSummary.CreatedAt) => request.Descending ? chains.OrderByDescending(x => x.CreatedAt).ToList() : chains.OrderBy(x => x.Version).ToList(), - nameof(SessionChainSummaryDto.LastSeenAt) => request.Descending + nameof(SessionChainSummary.LastSeenAt) => request.Descending ? chains.OrderByDescending(x => x.LastSeenAt).ToList() : chains.OrderBy(x => x.LastSeenAt).ToList(), - nameof(SessionChainSummaryDto.RevokedAt) => request.Descending + nameof(SessionChainSummary.RevokedAt) => request.Descending ? chains.OrderByDescending(x => x.RevokedAt).ToList() : chains.OrderBy(x => x.RevokedAt).ToList(), - nameof(SessionChainSummaryDto.DeviceType) => request.Descending + nameof(SessionChainSummary.DeviceType) => request.Descending ? chains.OrderByDescending(x => x.Device.DeviceType).ToList() : chains.OrderBy(x => x.Device.DeviceType).ToList(), - nameof(SessionChainSummaryDto.OperatingSystem) => request.Descending + nameof(SessionChainSummary.OperatingSystem) => request.Descending ? chains.OrderByDescending(x => x.Device.OperatingSystem).ToList() : chains.OrderBy(x => x.Device.OperatingSystem).ToList(), - nameof(SessionChainSummaryDto.Platform) => request.Descending + nameof(SessionChainSummary.Platform) => request.Descending ? chains.OrderByDescending(x => x.Device.Platform).ToList() : chains.OrderBy(x => x.Device.Platform).ToList(), - nameof(SessionChainSummaryDto.Browser) => request.Descending + nameof(SessionChainSummary.Browser) => request.Descending ? chains.OrderByDescending(x => x.Device.Browser).ToList() : chains.OrderBy(x => x.Device.Browser).ToList(), - nameof(SessionChainSummaryDto.RotationCount) => request.Descending + nameof(SessionChainSummary.RotationCount) => request.Descending ? chains.OrderByDescending(x => x.RotationCount).ToList() : chains.OrderBy(x => x.RotationCount).ToList(), - nameof(SessionChainSummaryDto.TouchCount) => request.Descending + nameof(SessionChainSummary.TouchCount) => request.Descending ? chains.OrderByDescending(x => x.TouchCount).ToList() : chains.OrderBy(x => x.TouchCount).ToList(), @@ -81,7 +81,7 @@ public async Task> GetUserChainsAsync(Access var pageItems = chains .Skip((request.PageNumber - 1) * request.PageSize) .Take(request.PageSize) - .Select(c => new SessionChainSummaryDto + .Select(c => new SessionChainSummary { ChainId = c.ChainId, DeviceType = c.Device.DeviceType, @@ -100,15 +100,15 @@ public async Task> GetUserChainsAsync(Access }) .ToList(); - return new PagedResult(pageItems, total, request.PageNumber, request.PageSize, request.SortBy, request.Descending); + return new PagedResult(pageItems, total, request.PageNumber, request.PageSize, request.SortBy, request.Descending); }); return await _accessOrchestrator.ExecuteAsync(context, command, ct); } - public async Task GetUserChainDetailAsync(AccessContext context, UserKey userKey, SessionChainId chainId, CancellationToken ct = default) + public async Task GetUserChainDetailAsync(AccessContext context, UserKey userKey, SessionChainId chainId, CancellationToken ct = default) { - var command = new AccessCommand(async innerCt => + var command = new AccessCommand(async innerCt => { var store = _storeFactory.Create(context.ResourceTenant); var chain = await store.GetChainAsync(chainId) ?? throw new UAuthNotFoundException("chain_not_found"); @@ -118,7 +118,7 @@ public async Task GetUserChainDetailAsync(AccessContext c var sessions = await store.GetSessionsByChainAsync(chainId); - return new SessionChainDetailDto + return new SessionChainDetail { ChainId = chain.ChainId, DeviceType = chain.Device.DeviceType, @@ -136,7 +136,7 @@ public async Task GetUserChainDetailAsync(AccessContext c Sessions = sessions .OrderByDescending(x => x.CreatedAt) - .Select(s => new SessionInfoDto( + .Select(s => new SessionInfo( s.SessionId, s.CreatedAt, s.ExpiresAt, diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs index caafda9e..cc247a8e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs @@ -41,6 +41,9 @@ public async Task LoginAsync(AuthFlowContext flow, AuthExecutionCon var effectiveFlow = execution.EffectiveClientProfile is null ? flow : await _authFlowContextFactory.RecreateWithClientProfileAsync(flow, (UAuthClientProfile)execution.EffectiveClientProfile, ct); + effectiveFlow = execution.Device is null + ? effectiveFlow + : await _authFlowContextFactory.RecreateWithDeviceAsync(effectiveFlow, execution.Device, ct); return await _loginOrchestrator.LoginAsync(effectiveFlow, request, ct); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs index 1c93e22e..172bad57 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs @@ -73,7 +73,7 @@ public async Task GetMyRolesAsync(HttpContext ctx) var accessContext = await _accessContextFactory.CreateAsync( flow, - action: UAuthActions.Authorization.Roles.ReadSelf, + action: UAuthActions.Authorization.Roles.GetSelf, resource: "authorization.roles", resourceId: flow.UserKey!.Value ); @@ -97,7 +97,7 @@ public async Task GetUserRolesAsync(UserKey userKey, HttpContext ctx) var accessContext = await _accessContextFactory.CreateAsync( flow, - action: UAuthActions.Authorization.Roles.ReadAdmin, + action: UAuthActions.Authorization.Roles.GetAdmin, resource: "authorization.roles", resourceId: userKey.Value ); diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialDto.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialInfo.cs similarity index 93% rename from src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialDto.cs rename to src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialInfo.cs index 2c3df24a..6141c3d0 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialDto.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialInfo.cs @@ -2,7 +2,7 @@ namespace CodeBeam.UltimateAuth.Credentials.Contracts; -public sealed record CredentialDto +public sealed record CredentialInfo { public Guid Id { get; set; } public CredentialType Type { get; init; } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs index ce621456..8a8a5315 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs @@ -2,5 +2,5 @@ public sealed record GetCredentialsResult { - public IReadOnlyCollection Credentials { get; init; } = Array.Empty(); + public IReadOnlyCollection Credentials { get; init; } = Array.Empty(); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs index 568a6788..eaaa8a65 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/CredentialManagementService.cs @@ -67,7 +67,7 @@ public async Task GetAllAsync(AccessContext context, Cance var dtos = credentials .OfType() - .Select(c => new CredentialDto + .Select(c => new CredentialInfo { Id = c.Id, Type = c.Type, diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierDto.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierInfo.cs similarity index 89% rename from src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierDto.cs rename to src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierInfo.cs index e8fc7fa7..4c3d381a 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierDto.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierInfo.cs @@ -2,7 +2,7 @@ namespace CodeBeam.UltimateAuth.Users.Contracts; -public sealed record UserIdentifierDto : IVersionedEntity +public sealed record UserIdentifierInfo : IVersionedEntity { public Guid Id { get; set; } public required UserIdentifierType Type { get; set; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs index 4694a8c6..7267c203 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs @@ -4,5 +4,6 @@ public enum UserIdentifierType { Username, Email, - Phone + Phone, + Custom } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusDto.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusInfo.cs similarity index 86% rename from src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusDto.cs rename to src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusInfo.cs index 736ab39b..ae13171f 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusDto.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusInfo.cs @@ -1,6 +1,6 @@ namespace CodeBeam.UltimateAuth.Users.Contracts; -public sealed record UserMfaStatusDto +public sealed record UserMfaStatusInfo { public bool IsEnabled { get; init; } public IReadOnlyCollection EnabledMethods { get; init; } = Array.Empty(); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs index a0def373..4a78263a 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs @@ -2,5 +2,5 @@ public sealed record GetUserIdentifiersResult { - public required IReadOnlyCollection Identifiers { get; init; } + public required IReadOnlyCollection Identifiers { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs index 88151a2b..a38424d4 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs @@ -144,9 +144,9 @@ public UserIdentifier MarkDeleted(DateTimeOffset at) return this; } - public UserIdentifierDto ToDto() + public UserIdentifierInfo ToDto() { - return new UserIdentifierDto() + return new UserIdentifierInfo() { Id = Id, Type = Type, diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs index f3ad993f..ff65fed1 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Users.Reference; public static class UserIdentifierMapper { - public static UserIdentifierDto ToDto(UserIdentifier record) + public static UserIdentifierInfo ToDto(UserIdentifier record) => new() { Id = record.Id, diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs index 0bdb2192..5c9157f1 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs @@ -15,9 +15,9 @@ public interface IUserApplicationService Task UpdateUserProfileAsync(AccessContext context, UpdateProfileRequest request, CancellationToken ct = default); - Task> GetIdentifiersByUserAsync(AccessContext context, UserIdentifierQuery query, CancellationToken ct = default); + Task> GetIdentifiersByUserAsync(AccessContext context, UserIdentifierQuery query, CancellationToken ct = default); - Task GetIdentifierAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default); + Task GetIdentifierAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default); Task UserIdentifierExistsAsync(AccessContext context, UserIdentifierType type, string value, IdentifierExistenceScope scope = IdentifierExistenceScope.TenantPrimaryOnly, CancellationToken ct = default); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index fbc7dce2..0044d49c 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -305,9 +305,9 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq #region Identifiers - public async Task> GetIdentifiersByUserAsync(AccessContext context, UserIdentifierQuery query, CancellationToken ct = default) + public async Task> GetIdentifiersByUserAsync(AccessContext context, UserIdentifierQuery query, CancellationToken ct = default) { - var command = new AccessCommand>(async innerCt => + var command = new AccessCommand>(async innerCt => { var targetUserKey = context.GetTargetUserKey(); @@ -317,7 +317,7 @@ public async Task> GetIdentifiersByUserAsync(Acce var result = await _identifierStore.QueryAsync(context.ResourceTenant, query, innerCt); var dtoItems = result.Items.Select(UserIdentifierMapper.ToDto).ToList().AsReadOnly(); - return new PagedResult( + return new PagedResult( dtoItems, result.TotalCount, result.PageNumber, @@ -329,9 +329,9 @@ public async Task> GetIdentifiersByUserAsync(Acce return await _accessOrchestrator.ExecuteAsync(context, command, ct); } - public async Task GetIdentifierAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default) + public async Task GetIdentifierAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default) { - var command = new AccessCommand(async innerCt => + var command = new AccessCommand(async innerCt => { var normalized = _identifierNormalizer.Normalize(type, value); if (!normalized.IsValid) @@ -365,7 +365,7 @@ public async Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifie { var command = new AccessCommand(async innerCt => { - var validationDto = new UserIdentifierDto() { Type = request.Type, Value = request.Value }; + var validationDto = new UserIdentifierInfo() { Type = request.Type, Value = request.Value }; var validationResult = await _identifierValidator.ValidateAsync(context, validationDto, innerCt); if (validationResult.IsValid != true) {