diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx index c449693f..9bef746e 100644 --- a/UltimateAuth.slnx +++ b/UltimateAuth.slnx @@ -4,16 +4,20 @@ - - - + + + + + + + @@ -35,6 +39,7 @@ + diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj index 3ac3c530..3e3e1fb5 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj @@ -11,10 +11,10 @@ - + - + diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj deleted file mode 100644 index 29faf8f7..00000000 --- a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net8.0;net9.0;net10.0 - $(NoWarn);1591 - - CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle - - - Provides a complete Entity Framework Core persistence setup for UltimateAuth. - This package includes reference domain implementations and Entity Framework Core-based persistence for all modules. - It is designed for production scenarios requiring durable storage. - - - authentication;authorization;identity;efcore;inmemory;bundle;auth-framework;security;jwt - uauthlogo.png - README.md - - - - - - - - - - - - - - - - - - diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj index 96d9adaa..92487581 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor new file mode 100644 index 00000000..2806b7d3 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor @@ -0,0 +1,19 @@ +@namespace CodeBeam.UltimateAuth.Sample +@inherits ComponentBase + + + + @if (Variant == UAuthLogoVariant.Brand) + { + + + + } + else + { + + + + } + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor.cs new file mode 100644 index 00000000..030d9b66 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogo.razor.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace CodeBeam.UltimateAuth.Sample; + +public partial class UAuthLogo : ComponentBase +{ + [Parameter] public UAuthLogoVariant Variant { get; set; } = UAuthLogoVariant.Brand; + + [Parameter] public int Size { get; set; } = 32; + + [Parameter] public string? ShieldColor { get; set; } = "#00072d"; + [Parameter] public string? KeyColor { get; set; } = "#f6f5ae"; + + [Parameter] public string? Class { get; set; } + [Parameter] public string? Style { get; set; } + + private string BuildStyle() + { + if (Variant == UAuthLogoVariant.Mono) + return $"color: {KeyColor}; {Style}"; + + return Style ?? ""; + } + + protected string KeyPath => @" +M120.43,39.44H79.57A11.67,11.67,0,0,0,67.9,51.11V77.37 +A11.67,11.67,0,0,0,79.57,89H90.51l3.89,3.9v5.32l-3.8,3.81v81.41H99 +v-5.33h13.69V169H108.1v-3.8H99C99,150.76,111.9,153,111.9,153 +V99.79h-8V93.32L108.19,89h12.24 +A11.67,11.67,0,0,0,132.1,77.37V51.11 +A11.67,11.67,0,0,0,120.43,39.44Z + +M79.57,48.19h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.84a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.84a2.91,2.91 0 0 1 2.91,-2.92Z + +M79.57,68.62h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.83a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.83a2.91,2.91 0 0 1 2.91,-2.92Z + +M114.59,48.19h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.84a2.91,2.91 0 0 1 -2.91,2.91 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.91 +v-5.84a2.92,2.92 0 0 1 2.92,-2.92Z + +M114.59,68.62h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.83a2.91,2.91 0 0 1 -2.91,2.92 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.92 +v-5.83a2.92,2.92 0 0 1 2.92,-2.92Z +"; +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogoVariant.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogoVariant.cs new file mode 100644 index 00000000..fe3be220 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Brand/UAuthLogoVariant.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample; + +public enum UAuthLogoVariant +{ + Brand, + Mono +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj new file mode 100644 index 00000000..f6e72309 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/App.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/App.razor new file mode 100644 index 00000000..3156b071 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/App.razor @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor new file mode 100644 index 00000000..ac680f3b --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor @@ -0,0 +1,101 @@ +@inherits UAuthHubLayoutComponentBase +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject NavigationManager Nav + +@if (!IsHubAuthorized) +{ + + + UltimateAuth + + UAuthHub Sample + + + + + + + + + + + + Access Denied + + + This page cannot be accessed directly. + UAuthHub login flows can only be initiated by an authorized client application. + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + + return; +} + + + + + UltimateAuth + + UAuthHub Sample + + + + + + + + + +
+ + + @((state.Identity?.DisplayName ?? "?").Trim() is var n ? (n.Length >= 2 ? n[..2] : n[..1]) : "?") + + +
+
+ + + @state.Identity?.DisplayName + @string.Join(", ", state.Claims.Roles) + + + + + + + + @if (state.Identity?.SessionState is not null && state.Identity.SessionState != SessionState.Active) + { + + + } + +
+
+ + + + +
+
+ + + @Body + +
+ + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor.cs new file mode 100644 index 00000000..e5886028 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/MainLayout.razor.cs @@ -0,0 +1,130 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Infrastructure; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Components.Layout; + +public partial class MainLayout +{ + [CascadingParameter] + public UAuthState UAuth { get; set; } = default!; + + [CascadingParameter] + public DarkModeManager DarkModeManager { get; set; } = default!; + + private async Task Refresh() + { + await UAuthClient.Flows.RefreshAsync(); + } + + private async Task Logout() + { + await UAuthClient.Flows.LogoutAsync(); + } + + private Color GetBadgeColor() + { + if (UAuth is null || !UAuth.IsAuthenticated) + return Color.Error; + + if (UAuth.IsStale) + return Color.Warning; + + var state = UAuth.Identity?.SessionState; + + if (state is null || state == SessionState.Active) + return Color.Success; + + if (state == SessionState.Invalid) + return Color.Error; + + return Color.Warning; + } + + private void HandleSignInClick() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login?focus=1", replace: true, forceLoad: true); + return; + } + + GoToLoginWithReturn(); + } + + private async Task Validate() + { + try + { + var result = await UAuthClient.Flows.ValidateAsync(); + + if (result.IsValid) + { + if (result.Snapshot?.Identity.UserStatus == UserStatus.SelfSuspended) + { + Snackbar.Add("Your account is suspended by you.", Severity.Warning); + return; + } + Snackbar.Add($"Session active • Tenant: {result.Snapshot?.Identity?.Tenant.Value} • User: {result.Snapshot?.Identity?.PrimaryUserName}", Severity.Success); + } + else + { + switch (result.State) + { + case SessionState.Expired: + Snackbar.Add("Session expired. Please sign in again.", Severity.Warning); + break; + + case SessionState.DeviceMismatch: + Snackbar.Add("Session invalid for this device.", Severity.Error); + break; + + default: + Snackbar.Add($"Session state: {result.State}", Severity.Error); + break; + } + } + } + catch (UAuthTransportException) + { + Snackbar.Add("Network error.", Severity.Error); + } + catch (UAuthProtocolException) + { + Snackbar.Add("Invalid response.", Severity.Error); + } + catch (UAuthException ex) + { + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Unexpected error: {ex.Message}", Severity.Error); + } + } + + private void GoToLoginWithReturn() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login", replace: true); + return; + } + + var current = Nav.ToBaseRelativePath(uri.ToString()); + if (string.IsNullOrWhiteSpace(current)) + current = "home"; + + var returnUrl = Uri.EscapeDataString("/" + current.TrimStart('/')); + Nav.NavigateTo($"/login?returnUrl={returnUrl}", replace: true); + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor new file mode 100644 index 00000000..e740b0c8 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Components/Layout/ReconnectModal.razor @@ -0,0 +1,31 @@ + + + +
+ +

+ Rejoining the server... +

+

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

+

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

+ +

+ The session has been paused by the server. +

+

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

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

Not Found

+

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

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

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

+ This action can't be undone.

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

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

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

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

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

+

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

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/_Imports.razor b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/_Imports.razor new file mode 100644 index 00000000..ed7e1a53 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Components/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using CodeBeam.UAuth.Sample.IntWasm +@using CodeBeam.UAuth.Sample.IntWasm.Client +@using CodeBeam.UAuth.Sample.IntWasm.Components + +@using MudBlazor +@using MudExtensions diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs new file mode 100644 index 00000000..7790e14a --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Program.cs @@ -0,0 +1,65 @@ +using CodeBeam.UAuth.Sample.IntWasm.Client.Infrastructure; +using CodeBeam.UAuth.Sample.IntWasm.Client.ResourceApi; +using CodeBeam.UAuth.Sample.IntWasm.Components; +using CodeBeam.UltimateAuth.Client.AspNetCore; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; +using CodeBeam.UltimateAuth.Core.Domain; +using MudBlazor.Services; +using MudExtensions.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); +builder.Services.AddMudExtensions(); + +builder.Services.AddUltimateAuthClientBlazor(o => +{ + o.Endpoints.BasePath = "https://localhost:6112/auth"; // UAuthHub EFCore URL + o.Reauth.Behavior = ReauthBehavior.RaiseEvent; + o.Login.AllowCredentialPost = true; + o.Pkce.ReturnUrl = "https://localhost:6132/home"; // This application domain + path +}); + +builder.Services.AddScoped(); + +builder.Services.AddUltimateAuthAspNetCoreCompatibility(); +// If you want to use the UltimateAuthClientBlazor without the AspNetCore compatibility layer, you can register the services like this instead: +// It gives full AspNetCore compatibility but affects performance significantly. +//builder.Services.AddUltimateAuthResourceApi(o => o.UAuthHubBaseUrl = "https://localhost:6112/auth"); + +builder.Services.AddScoped(); + +builder.Services.AddHttpClient("resourceApi", client => +{ + client.BaseAddress = new Uri("https://localhost:6122"); // Resource API URL +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} +app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); +app.UseHttpsRedirection(); + +//app.UseUltimateAuthResourceApiWithAspNetCore(); +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(CodeBeam.UAuth.Sample.IntWasm.Client._Imports).Assembly, UAuthAssemblies.BlazorClient().First()); + +app.Run(); diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Properties/launchSettings.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Properties/launchSettings.json new file mode 100644 index 00000000..40513f8d --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:6133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:6132;http://localhost:6133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.Development.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.json b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/UltimateAuth-Logo.png b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/UltimateAuth-Logo.png new file mode 100644 index 00000000..5b7282f1 Binary files /dev/null and b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/UltimateAuth-Logo.png differ diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/app.css b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/app.css new file mode 100644 index 00000000..5b69be65 --- /dev/null +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/wwwroot/app.css @@ -0,0 +1,206 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} + +.uauth-stack { + min-height: 60vh; + max-height: calc(100vh - var(--mud-appbar-height)); + width: 30vw; + min-width: 300px; +} + +.uauth-menu-popover { + width: 300px; +} + +.uauth-login-paper { + min-height: 70vh; +} + + .uauth-login-paper.mud-theme-primary { + background: linear-gradient(145deg, var(--mud-palette-primary), rgba(0, 0, 0, 0.85) ); + color: white; + } + +.uauth-brand-glow { + filter: drop-shadow(0 0 25px rgba(255,255,255,0.15)); +} + +.uauth-logo-slide { + animation: uauth-logo-float 30s ease-in-out infinite; +} + +.uauth-text-transform-none .mud-button { + text-transform: none; +} + +.uauth-dialog { + height: 68vh; + max-height: 68vh; + overflow: auto; +} + +.text-secondary { + color: var(--mud-palette-text-secondary); +} + +.uauth-blur { + backdrop-filter: blur(10px); +} + +.uauth-blur-slight { + backdrop-filter: blur(4px); +} + +@keyframes uauth-logo-float { + 0% { + transform: translateY(0) rotateY(0); + } + + 10% { + transform: translateY(0) rotateY(0); + } + + 15% { + transform: translateY(200px) rotateY(360deg); + } + + 35% { + transform: translateY(200px) rotateY(360deg); + } + + 40% { + transform: translateY(200px) rotateY(720deg); + } + + 60% { + transform: translateY(200px) rotateY(720deg); + } + + 65% { + transform: translateY(0) rotateY(360deg); + } + + 85% { + transform: translateY(0) rotateY(360deg); + } + + 90% { + transform: translateY(0) rotateY(0); + } + + 100% { + transform: translateY(0) rotateY(0); + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj new file mode 100644 index 00000000..2d474a92 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.http b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.http new file mode 100644 index 00000000..506fcd1e --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.http @@ -0,0 +1,6 @@ +@CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore_HostAddress = http://localhost:5151 + +GET {{CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Program.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Program.cs new file mode 100644 index 00000000..b1825cb2 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Program.cs @@ -0,0 +1,28 @@ +using CodeBeam.UltimateAuth.Server.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +builder.Services.AddUltimateAuthResourceApi(o => +{ + o.UAuthHubBaseUrl = "https://localhost:6112"; + o.AllowedClientOrigins.Add("https://localhost:6132"); + o.CorsPolicyName = "resourceApi"; +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseUltimateAuthResourceApiWithAspNetCore(); + +app.MapControllers(); + +app.Run(); diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Properties/launchSettings.json b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Properties/launchSettings.json new file mode 100644 index 00000000..00407688 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:6123", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:6122;http://localhost:6123", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/AppActions.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/AppActions.cs new file mode 100644 index 00000000..7d9191f3 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/AppActions.cs @@ -0,0 +1,18 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Defaults; + +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +public static class AppActions +{ + public static class Products + { + public static readonly string Read = UAuthActions.Create("products", "read", ActionScope.Self); + + public static readonly string Create = UAuthActions.Create("products", "create", ActionScope.Admin); + + public static readonly string Update = UAuthActions.Create("products", "update", ActionScope.Admin); + + public static readonly string Delete = UAuthActions.Create("products", "delete", ActionScope.Admin); + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductStore.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductStore.cs new file mode 100644 index 00000000..10db05bf --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductStore.cs @@ -0,0 +1,6 @@ +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +public static class ProductStore +{ + public static List Items = new() { new SampleProduct() { Id = 0, Name = "Test"} }; +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductsController.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductsController.cs new file mode 100644 index 00000000..8f99886a --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/ProductsController.cs @@ -0,0 +1,73 @@ +using CodeBeam.UltimateAuth.Core.Defaults; +using CodeBeam.UltimateAuth.Core.Errors; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +[ApiController] +[Route("api/products")] +public class ProductsController : ControllerBase +{ + [HttpGet] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] // You can use UAuthActions as permission in ASP.NET Core policy. + public IActionResult GetAll() + { + return Ok(ProductStore.Items); + } + + [HttpGet("{id}")] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Get(int id) + { + var item = ProductStore.Items.FirstOrDefault(x => x.Id == id); + if (item == null) return NotFound(); + + return Ok(item); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Create(SampleProduct product) + { + var nextId = ProductStore.Items.Any() + ? ProductStore.Items.Max(x => x.Id) + 1 + : 1; + + product.Id = nextId; + ProductStore.Items.Add(product); + + return Ok(product); + } + + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Update(int id, SampleProduct product) + { + var item = ProductStore.Items.FirstOrDefault(x => x.Id == id); + + if (item == null) + { + throw new UAuthNotFoundException("No product found."); + } + + item.Name = product.Name; + return Ok(product); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + //[Authorize(Policy = UAuthActions.Authorization.Roles.GetAdmin)] + public IActionResult Delete(int id) + { + var item = ProductStore.Items.FirstOrDefault(x => x.Id == id); + if (item == null) return NotFound(); + + ProductStore.Items.Remove(item); + return Ok(item); + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/SampleProduct.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/SampleProduct.cs new file mode 100644 index 00000000..2c75603d --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/SampleData/SampleProduct.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample.ResourceApi; + +public class SampleProduct +{ + public int Id { get; set; } + public string Name { get; set; } = default!; +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.Development.json b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.json b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs index a59b1f61..0b595429 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs @@ -7,6 +7,7 @@ builder.Services.AddUltimateAuthResourceApi(o => { + // TODO: Make multiple UAuthHub support via resolver, then different client apps can use different UAuthHub instances if needed. o.UAuthHubBaseUrl = "https://localhost:6110"; o.AllowedClientOrigins.Add("https://localhost:6130"); }); diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs index d6986a1e..290b399e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs @@ -77,7 +77,10 @@ UAuthMode.SemiHybrid or if (persistence == RefreshTokenPersistence.Persist) { var store = _storeFactory.Create(flow.Tenant); - await store.StoreAsync(stored, ct); + await store.ExecuteAsync(async ct => + { + await store.StoreAsync(stored, ct); + }); } return new RefreshTokenInfo diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs index 3a0b5a30..b6f8d893 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs @@ -214,9 +214,7 @@ public async Task> GetByIdsAsync( return result.AsReadOnly(); } - public async Task> QueryAsync( - RoleQuery query, - CancellationToken ct = default) + public async Task> QueryAsync(RoleQuery query, CancellationToken ct = default) { var normalized = query.Normalize(); @@ -257,8 +255,25 @@ public async Task> QueryAsync( .Take(normalized.PageSize) .ToListAsync(ct); + var roleIds = items.Select(x => x.Id).ToList(); + + var permissions = await DbSetPermission + .AsNoTracking() + .Where(x => + x.Tenant == _tenant && + roleIds.Contains(x.RoleId)) + .ToListAsync(ct); + + var lookup = permissions + .GroupBy(x => x.RoleId) + .ToDictionary(x => x.Key); + var result = items - .Select(x => RoleMapper.ToDomain(x, Enumerable.Empty())) + .Select(x => + { + lookup.TryGetValue(x.Id, out var perms); + return RoleMapper.ToDomain(x, perms ?? Enumerable.Empty()); + }) .ToList() .AsReadOnly(); diff --git a/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj new file mode 100644 index 00000000..17220e06 --- /dev/null +++ b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj @@ -0,0 +1,36 @@ + + + + net8.0;net9.0;net10.0 + $(NoWarn);1591 + + CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle + + + Provides a complete Entity Framework Core persistence setup for UltimateAuth. + This package includes reference domain implementations and Entity Framework Core-based persistence for all modules. + It is designed for production scenarios requiring durable storage. + + + authentication;authorization;identity;efcore;inmemory;bundle;auth-framework;security;jwt + uauthlogo.png + README.md + + + + + + + + + + + + + + + + + + + diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Data/UAuthDbContext.cs b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Data/UAuthDbContext.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Data/UAuthDbContext.cs rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Data/UAuthDbContext.cs diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Options/UAuthEfCoreOptions.cs b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Options/UAuthEfCoreOptions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Options/UAuthEfCoreOptions.cs rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/Options/UAuthEfCoreOptions.cs diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/README.md b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/README.md similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/README.md rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/README.md diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/uauthlogo.png b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/uauthlogo.png similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/uauthlogo.png rename to src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/uauthlogo.png diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj similarity index 55% rename from nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.Bundle.csproj rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj index 45fc3e18..c0c22ad8 100644 --- a/nuget/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.Bundle.csproj +++ b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj @@ -20,12 +20,12 @@ - - - - - - + + + + + + diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/README.md b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/README.md similarity index 100% rename from nuget/CodeBeam.UltimateAuth.InMemory/README.md rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/README.md diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/UltimateAuthInMemoryExtensions.cs b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/UltimateAuthInMemoryExtensions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.InMemory/UltimateAuthInMemoryExtensions.cs rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/UltimateAuthInMemoryExtensions.cs diff --git a/nuget/CodeBeam.UltimateAuth.InMemory/uauthlogo.png b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/uauthlogo.png similarity index 100% rename from nuget/CodeBeam.UltimateAuth.InMemory/uauthlogo.png rename to src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/uauthlogo.png diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj similarity index 66% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj index e8cde4d1..01f6356a 100644 --- a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj +++ b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/README.md b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/README.md similarity index 100% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/README.md rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/README.md diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/UltimateAuthReferenceBundleExtensions.cs diff --git a/nuget/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png similarity index 100% rename from nuget/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png rename to src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/uauthlogo.png diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj new file mode 100644 index 00000000..a6b3ab90 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj @@ -0,0 +1,28 @@ + + + + net8.0;net9.0;net10.0 + $(NoWarn);1591 + + CodeBeam.UltimateAuth.Client.AspNetCore + + + Provides ASP.NET Core compatibility layer for UltimateAuth client applications. + This package enables integration with ASP.NET Core authentication and authorization infrastructure without introducing server-side authentication overhead. It allows applications to use features like [Authorize] without requiring a full server-side auth pipeline. + Designed for Blazor Web App (Interactive WASM) and similar hybrid hosting scenarios. + This package does NOT implement authentication. It only provides a lightweight compatibility layer. + + authentication;authorization;identity;aspnetcore;auth;jwt;auth-framework + uauthlogo.png + README.md + + + + + + + + + + + diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/NoOpAuthHandler.cs b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/NoOpAuthHandler.cs new file mode 100644 index 00000000..1c92c535 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/NoOpAuthHandler.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Encodings.Web; + +namespace CodeBeam.UltimateAuth.Client.AspNetCore; + +public class NoopAuthHandler : AuthenticationHandler +{ + public NoopAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/README.md b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/README.md new file mode 100644 index 00000000..8a3bad2a --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/README.md @@ -0,0 +1,30 @@ +# UltimateAuth Client ASP.NET Core Compatibility + +Provides a lightweight ASP.NET Core compatibility layer for UltimateAuth client applications. + +--- + +## 🎯 Purpose + +This package enables seamless integration between **UltimateAuth client-side authentication** and **ASP.NET Core infrastructure**. + +It allows applications to: + +- Use `[Authorize]` without runtime errors +- Integrate with ASP.NET Core middleware pipeline +- Avoid mandatory server-side authentication setup + +--- + +## ⚠️ Important + +This package does **NOT** perform authentication. + +It only provides a **No-op compatibility layer** for ASP.NET Core. + +```text +✔ Prevents ASP.NET Core auth errors +✔ Enables framework compatibility +❌ Does NOT validate users +❌ Does NOT populate HttpContext.User +❌ Does NOT enforce security \ No newline at end of file diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/ServiceCollectionExtensions.cs b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b546b9d5 --- /dev/null +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.Client.AspNetCore; + +public static class UAuthAspNetCoreExtensions +{ + public static IServiceCollection AddUltimateAuthAspNetCoreCompatibility( + this IServiceCollection services) + { + services.AddAuthentication("UAuth.Noop") + .AddScheme("UAuth.Noop", _ => { }); + + services.AddAuthorization(); + + return services; + } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/uauthlogo.png b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/uauthlogo.png new file mode 100644 index 00000000..911f2530 Binary files /dev/null and b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/uauthlogo.png differ diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj index ee936bd7..1b44b87b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj @@ -20,8 +20,8 @@ - - + +