diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx
index 350cadd2..22c26b0d 100644
--- a/UltimateAuth.slnx
+++ b/UltimateAuth.slnx
@@ -12,6 +12,7 @@
+
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj
index 13ddab2b..32b6af51 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
@@ -14,6 +14,7 @@
+
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
index 22f00b6f..f3f88149 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
@@ -1,3 +1,4 @@
+using CodeBeam.UltimateAuth.Authentication.InMemory;
using CodeBeam.UltimateAuth.Authorization.InMemory;
using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions;
using CodeBeam.UltimateAuth.Authorization.Reference.Extensions;
@@ -8,8 +9,6 @@
using CodeBeam.UltimateAuth.Credentials.Reference;
using CodeBeam.UltimateAuth.Sample.UAuthHub.Components;
using CodeBeam.UltimateAuth.Security.Argon2;
-using CodeBeam.UltimateAuth.Server.Authentication;
-using CodeBeam.UltimateAuth.Server.Defaults;
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Sessions.InMemory;
using CodeBeam.UltimateAuth.Tokens.InMemory;
@@ -57,6 +56,7 @@
.AddUltimateAuthAuthorizationReference()
.AddUltimateAuthInMemorySessions()
.AddUltimateAuthInMemoryTokens()
+ .AddUltimateAuthInMemoryAuthenticationSecurity()
.AddUltimateAuthArgon2();
builder.Services.AddUltimateAuthClient(o =>
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 34314a51..7cc56f81 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
@@ -15,6 +15,7 @@
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Common/UAuthDialog.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Common/UAuthDialog.cs
new file mode 100644
index 00000000..58dae70b
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Common/UAuthDialog.cs
@@ -0,0 +1,29 @@
+using CodeBeam.UltimateAuth.Client;
+using CodeBeam.UltimateAuth.Core.Domain;
+using MudBlazor;
+
+namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Common;
+
+public static class UAuthDialog
+{
+ public static DialogParameters GetDialogParameters(UAuthState state, UserKey? userKey = null)
+ {
+ DialogParameters parameters = new DialogParameters();
+ parameters.Add("AuthState", state);
+ if (userKey != null )
+ {
+ parameters.Add("UserKey", userKey);
+ }
+ return parameters;
+ }
+
+ public static DialogOptions GetDialogOptions(MaxWidth maxWidth = MaxWidth.Medium)
+ {
+ return new DialogOptions
+ {
+ MaxWidth = maxWidth,
+ FullWidth = true,
+ CloseButton = true
+ };
+ }
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Custom/UAuthPageComponent.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Custom/UAuthPageComponent.razor
new file mode 100644
index 00000000..5af543e4
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Custom/UAuthPageComponent.razor
@@ -0,0 +1,10 @@
+
+
+ @ChildContent
+
+
+
+@code {
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor
new file mode 100644
index 00000000..7873ce91
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/AccountStatusDialog.razor
@@ -0,0 +1,93 @@
+@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
+
+
+
+
+
+@code {
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ private async Task SuspendAccountAsync()
+ {
+ var info = await DialogService.ShowMessageBoxAsync(
+ title: "Are You Sure",
+ markupMessage: (MarkupString)
+ """
+ You are going to suspend your account.
+ You can still active your account later.
+ """,
+ yesText: "Suspend", noText: "Cancel",
+ options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" });
+
+ if (info != true)
+ {
+ Snackbar.Add("Suspend process cancelled", Severity.Info);
+ return;
+ }
+
+ ChangeUserStatusSelfRequest request = new() { NewStatus = SelfUserStatus.SelfSuspended };
+ var result = await UAuthClient.Users.ChangeStatusSelfAsync(request);
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Your account suspended successfully.", Severity.Success);
+ MudDialog.Close();
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Delete failed.", Severity.Error);
+ }
+ }
+
+ private async Task DeleteAccountAsync()
+ {
+ var info = await DialogService.ShowMessageBoxAsync(
+ title: "Are You Sure",
+ markupMessage: (MarkupString)
+ """
+ You are going to delete your account.
+ This action can't be undone.
+ (Actually it is, admin can handle soft deleted accounts.)
+ """,
+ yesText: "Delete", noText: "Cancel",
+ options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" });
+
+ if (info != true)
+ {
+ Snackbar.Add("Deletion cancelled", Severity.Info);
+ return;
+ }
+
+ var result = await UAuthClient.Users.DeleteMeAsync();
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Your account deleted successfully.", Severity.Success);
+ MudDialog.Close();
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Delete failed.", Severity.Error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor
new file mode 100644
index 00000000..479a08bd
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CreateUserDialog.razor
@@ -0,0 +1,76 @@
+@using CodeBeam.UltimateAuth.Credentials.Contracts
+@using CodeBeam.UltimateAuth.Users.Contracts
+@inject IUAuthClient UAuthClient
+@inject ISnackbar Snackbar
+
+
+
+ Create User
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ Create
+
+
+
+@code {
+ private MudForm _form = null!;
+ private string? _username;
+ private string? _email;
+ private string? _password;
+ private string? _passwordCheck;
+ private string? _displayName;
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ private async Task CreateUserAsync()
+ {
+ await _form.Validate();
+
+ if (!_form.IsValid)
+ return;
+
+ if (_password != _passwordCheck)
+ {
+ Snackbar.Add("Passwords do not match", Severity.Error);
+ return;
+ }
+
+ var request = new CreateUserRequest
+ {
+ UserName = _username,
+ Email = _email,
+ DisplayName = _displayName,
+ Password = _password
+ };
+
+ var result = await UAuthClient.Users.CreateAdminAsync(request);
+
+ if (!result.IsSuccess)
+ {
+ Snackbar.Add(result.GetErrorText ?? "User creation failed", Severity.Error);
+ return;
+ }
+
+ Snackbar.Add("User created successfully", Severity.Success);
+ MudDialog.Close(DialogResult.Ok(true));
+ }
+
+ private string PasswordMatch(string? arg) => _password != arg ? "Passwords don't match" : string.Empty;
+
+ private void Cancel() => MudDialog.Cancel();
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor
new file mode 100644
index 00000000..01c01ff0
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor
@@ -0,0 +1,134 @@
+@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
+
+
+
+@code {
+ private MudForm _form = null!;
+ private string? _oldPassword;
+ private string? _newPassword;
+ private string? _newPasswordCheck;
+ private bool _passwordMode1 = false;
+ private bool _passwordMode2 = false;
+ private bool _passwordMode3 = true;
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ [Parameter]
+ public UserKey? UserKey { get; set; }
+
+ private async Task ChangePasswordAsync()
+ {
+ if (_form is null)
+ return;
+
+ await _form.Validate();
+ if (!_form.IsValid)
+ {
+ Snackbar.Add("Form is not valid.", Severity.Error);
+ return;
+ }
+
+ if (_newPassword != _newPasswordCheck)
+ {
+ Snackbar.Add("New password and check do not match", Severity.Error);
+ return;
+ }
+
+ ChangeCredentialRequest request;
+
+ if (UserKey is null)
+ {
+ request = new ChangeCredentialRequest
+ {
+ CurrentSecret = _oldPassword!,
+ NewSecret = _newPassword!
+ };
+ }
+ else
+ {
+ request = new ChangeCredentialRequest
+ {
+ NewSecret = _newPassword!
+ };
+ }
+
+ UAuthResult result;
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Credentials.ChangeMyAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Credentials.ChangeCredentialAsync(UserKey.Value, request);
+ }
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Password changed successfully", Severity.Success);
+ _oldPassword = null;
+ _newPassword = null;
+ _newPasswordCheck = null;
+ MudDialog.Close(DialogResult.Ok(true));
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "An error occurred while changing password", Severity.Error);
+ }
+ }
+
+ private string PasswordMatch(string arg) => _newPassword != arg ? "Passwords don't match" : string.Empty;
+
+ private void Cancel() => MudDialog.Cancel();
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor
index 6aba7ba5..808e7fda 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor
@@ -1,49 +1,72 @@
-@using CodeBeam.UltimateAuth.Users.Contracts
+@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
+
+ @if (UserKey is null)
+ {
+ User: @AuthState?.Identity?.DisplayName
+ }
+ else
+ {
+ UserKey: @UserKey.Value
+ }
+
-
+
- Identifiers
+
+ Identifiers
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
@if (context.Item.IsPrimary)
{
-
-
+
+
}
else
{
-
-
+
+
}
-
-
+
+
-
-
+
+
@@ -54,27 +77,41 @@
-
-
-
-
-
- Add
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
-
-
+
Cancel
- OK
@code {
+ private MudDataGrid? _grid;
private UserIdentifierType _newIdentifierType;
private string? _newIdentifierValue;
+ private bool _newIdentifierPrimary;
+ private bool _loading = false;
+ private bool _reloadQueued;
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = default!;
@@ -82,7 +119,8 @@
[Parameter]
public UAuthState AuthState { get; set; } = default!;
- private List _identifiers = new();
+ [Parameter]
+ public UserKey? UserKey { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -93,12 +131,86 @@
var result = await UAuthClient.Identifiers.GetMyIdentifiersAsync();
if (result != null && result.IsSuccess && result.Value != null)
{
- _identifiers = result.Value.ToList();
+ await ReloadAsync();
StateHasChanged();
}
}
}
+ private async Task> LoadServerData(GridState state, CancellationToken ct)
+ {
+ var sort = state.SortDefinitions?.FirstOrDefault();
+
+ var req = new PageRequest
+ {
+ PageNumber = state.Page + 1,
+ PageSize = state.PageSize,
+ SortBy = sort?.SortBy,
+ Descending = sort?.Descending ?? false
+ };
+
+ UAuthResult> res;
+
+ if (UserKey is null)
+ {
+ res = await UAuthClient.Identifiers.GetMyIdentifiersAsync(req);
+ }
+ else
+ {
+ res = await UAuthClient.Identifiers.GetUserIdentifiersAsync(UserKey.Value, req);
+ }
+
+ if (!res.IsSuccess || res.Value is null)
+ {
+ Snackbar.Add(res.Problem?.Title ?? "Failed", Severity.Error);
+
+ return new GridData
+ {
+ Items = Array.Empty(),
+ TotalItems = 0
+ };
+ }
+
+ return new GridData
+ {
+ Items = res.Value.Items,
+ TotalItems = res.Value.TotalCount
+ };
+ }
+
+ private async Task ReloadAsync()
+ {
+ if (_loading)
+ {
+ _reloadQueued = true;
+ return;
+ }
+
+ if (_grid is null)
+ return;
+
+ _loading = true;
+ await InvokeAsync(StateHasChanged);
+ await Task.Delay(300);
+
+ try
+ {
+ await _grid.ReloadServerData();
+ }
+ finally
+ {
+ _loading = false;
+
+ if (_reloadQueued)
+ {
+ _reloadQueued = false;
+ await ReloadAsync();
+ }
+
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
private async Task CommittedItemChanges(UserIdentifierDto item)
{
UpdateUserIdentifierRequest updateRequest = new()
@@ -106,16 +218,28 @@
Id = item.Id,
NewValue = item.Value
};
- var result = await UAuthClient.Identifiers.UpdateSelfAsync(updateRequest);
+
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Identifiers.UpdateSelfAsync(updateRequest);
+ }
+ else
+ {
+ result = await UAuthClient.Identifiers.UpdateAdminAsync(UserKey.Value, updateRequest);
+ }
+
if (result.IsSuccess)
{
Snackbar.Add("Identifier updated successfully", Severity.Success);
}
else
{
- Snackbar.Add("Failed to update identifier", Severity.Error);
+ Snackbar.Add(result?.GetErrorText ?? "Failed to update identifier", Severity.Error);
}
+ await ReloadAsync();
return DataGridEditFormAction.Close;
}
@@ -130,92 +254,153 @@
AddUserIdentifierRequest request = new()
{
Type = _newIdentifierType,
- Value = _newIdentifierValue
+ Value = _newIdentifierValue,
+ IsPrimary = _newIdentifierPrimary
};
- var result = await UAuthClient.Identifiers.AddSelfAsync(request);
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Identifiers.AddSelfAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Identifiers.AddAdminAsync(UserKey.Value, request);
+ }
+
if (result.IsSuccess)
{
Snackbar.Add("Identifier added successfully", Severity.Success);
- var getResult = await UAuthClient.Identifiers.GetMyIdentifiersAsync();
- _identifiers = getResult.Value?.ToList() ?? new List();
+ await ReloadAsync();
StateHasChanged();
}
else
{
- Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to add identifier", Severity.Error);
+ Snackbar.Add(result?.GetErrorText ?? "Failed to add identifier", Severity.Error);
}
}
- private async Task SetPrimaryAsync(Guid id)
+ private async Task VerifyAsync(Guid id)
{
- SetPrimaryUserIdentifierRequest request = new() { IdentifierId = id };
- var result = await UAuthClient.Identifiers.SetPrimarySelfAsync(request);
+ var demoInfo = await DialogService.ShowMessageBoxAsync(
+ title: "Demo verification",
+ markupMessage: (MarkupString)
+ """
+ This is a demo action.
+ In a real app, you should verify identifiers via Email, SMS, or an Authenticator flow.
+ This will only mark the identifier as verified in UltimateAuth.
+ """,
+ yesText: "Verify", noText: "Cancel",
+ options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" });
+
+ if (demoInfo != true)
+ {
+ Snackbar.Add("Verification cancelled", Severity.Info);
+ return;
+ }
+
+ VerifyUserIdentifierRequest request = new() { IdentifierId = id };
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Identifiers.VerifySelfAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Identifiers.VerifyAdminAsync(UserKey.Value, request);
+ }
+
if (result.IsSuccess)
{
- Snackbar.Add("Primary identifier set successfully", Severity.Success);
- var getResult = await UAuthClient.Identifiers.GetMyIdentifiersAsync();
- _identifiers = getResult.Value?.ToList() ?? new List();
+ Snackbar.Add("Identifier verified successfully", Severity.Success);
+ await ReloadAsync();
StateHasChanged();
}
else
{
- Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to set primary identifier", Severity.Error);
+ Snackbar.Add(result?.GetErrorText ?? "Failed to verify primary identifier", Severity.Error);
}
}
- private async Task VerifyAsync(Guid id)
+ private async Task SetPrimaryAsync(Guid id)
{
- VerifyUserIdentifierRequest request = new() { IdentifierId = id };
- var result = await UAuthClient.Identifiers.VerifySelfAsync(request);
+ SetPrimaryUserIdentifierRequest request = new() { IdentifierId = id };
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Identifiers.SetPrimarySelfAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Identifiers.SetPrimaryAdminAsync(UserKey.Value, request);
+ }
+
if (result.IsSuccess)
{
- Snackbar.Add("Primary identifier verified successfully", Severity.Success);
- var getResult = await UAuthClient.Identifiers.GetMyIdentifiersAsync();
- _identifiers = getResult.Value?.ToList() ?? new List();
+ Snackbar.Add("Primary identifier set successfully", Severity.Success);
+ await ReloadAsync();
StateHasChanged();
}
else
{
- Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to verify primary identifier", Severity.Error);
+ Snackbar.Add(result?.GetErrorText ?? "Failed to set primary identifier", Severity.Error);
}
}
private async Task UnsetPrimaryAsync(Guid id)
{
UnsetPrimaryUserIdentifierRequest request = new() { IdentifierId = id };
- var result = await UAuthClient.Identifiers.UnsetPrimarySelfAsync(request);
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Identifiers.UnsetPrimarySelfAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Identifiers.UnsetPrimaryAdminAsync(UserKey.Value, request);
+ }
+
if (result.IsSuccess)
{
Snackbar.Add("Primary identifier unset successfully", Severity.Success);
- var getResult = await UAuthClient.Identifiers.GetMyIdentifiersAsync();
- _identifiers = getResult.Value?.ToList() ?? new List();
+ await ReloadAsync();
StateHasChanged();
}
else
{
- Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to unset primary identifier", Severity.Error);
+ Snackbar.Add(result?.GetErrorText ?? "Failed to unset primary identifier", Severity.Error);
}
}
private async Task DeleteIdentifier(Guid id)
{
DeleteUserIdentifierRequest request = new() { IdentifierId = id };
- var result = await UAuthClient.Identifiers.DeleteSelfAsync(request);
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Identifiers.DeleteSelfAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Identifiers.DeleteAdminAsync(UserKey.Value, request);
+ }
+
if (result.IsSuccess)
{
Snackbar.Add("Identifier deleted successfully", Severity.Success);
- var getResult = await UAuthClient.Identifiers.GetMyIdentifiersAsync();
- _identifiers = getResult.Value?.ToList() ?? new List();
+ await ReloadAsync();
StateHasChanged();
}
else
{
- Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to delete identifier", Severity.Error);
+ Snackbar.Add(result?.GetErrorText ?? "Failed to delete identifier", Severity.Error);
}
}
- private void Submit() => MudDialog.Close(DialogResult.Ok(true));
-
private void Cancel() => MudDialog.Cancel();
}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor
new file mode 100644
index 00000000..77cf7c13
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor
@@ -0,0 +1,160 @@
+@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
+
+
+
+@code {
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public RoleInfo Role { get; set; } = default!;
+
+ private List _groups = new();
+
+ protected override void OnInitialized()
+ {
+ var catalog = UAuthPermissionCatalog.GetAdminPermissions();
+ var expanded = PermissionExpander.Expand(Role.Permissions, catalog);
+ var selected = expanded.Select(x => x.Value).ToHashSet();
+
+ _groups = catalog
+ .GroupBy(p => p.Split('.')[0])
+ .Select(g => new PermissionGroup
+ {
+ Name = g.Key,
+ Items = g.Select(p => new PermissionItem
+ {
+ Value = p,
+ Selected = selected.Contains(p)
+ }).ToList()
+ })
+ .OrderBy(x => x.Name)
+ .ToList();
+ }
+
+ void ToggleGroup(PermissionGroup group, bool value)
+ {
+ foreach (var item in group.Items)
+ item.Selected = value;
+ }
+
+ void TogglePermission(PermissionItem item, bool value)
+ {
+ item.Selected = value;
+ }
+
+ bool? GetGroupState(PermissionGroup group)
+ {
+ var selected = group.Items.Count(x => x.Selected);
+
+ if (selected == 0)
+ return false;
+
+ if (selected == group.Items.Count)
+ return true;
+
+ return null;
+ }
+
+ private async Task Save()
+ {
+ var permissions = _groups.SelectMany(g => g.Items).Where(x => x.Selected).Select(x => Permission.From(x.Value)).ToList();
+
+ var req = new SetPermissionsRequest
+ {
+ Permissions = permissions
+ };
+
+ var result = await UAuthClient.Authorization.SetPermissionsAsync(Role.Id, req);
+
+ if (!result.IsSuccess)
+ {
+ Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Failed to update permissions", Severity.Error);
+ return;
+ }
+
+ var result2 = await UAuthClient.Authorization.QueryRolesAsync(new RoleQuery() { Search = Role.Name });
+ if (result2.Value?.Items is not null)
+ {
+ Role = result2.Value.Items.First();
+ }
+
+ Snackbar.Add("Permissions updated", Severity.Success);
+ RefreshUI();
+ }
+
+ private void RefreshUI()
+ {
+ var catalog = UAuthPermissionCatalog.GetAdminPermissions();
+ var expanded = PermissionExpander.Expand(Role.Permissions, catalog);
+ var selected = expanded.Select(x => x.Value).ToHashSet();
+
+ foreach (var group in _groups)
+ {
+ foreach (var item in group.Items)
+ {
+ item.Selected = selected.Contains(item.Value);
+ }
+ }
+
+ StateHasChanged();
+ }
+
+ private void Cancel() => MudDialog.Cancel();
+
+ private class PermissionGroup
+ {
+ public string Name { get; set; } = "";
+ public List Items { get; set; } = new();
+ }
+
+ private class PermissionItem
+ {
+ public string Value { get; set; } = "";
+ public bool Selected { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor
new file mode 100644
index 00000000..7a02975b
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor
@@ -0,0 +1,199 @@
+@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
+ }
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Personal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Localization
+
+
+
+
+
+
+
+
+
+
+ @foreach (var tz in TimeZoneInfo.GetSystemTimeZones())
+ {
+ @tz.Id - @tz.DisplayName
+ }
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ Save
+
+
+
+@code {
+ private MudForm? _form;
+ private string? _firstName;
+ private string? _lastName;
+ private string? _displayName;
+ private DateTime? _birthDate;
+ private string? _gender;
+ private string? _bio;
+ private string? _language;
+ private string? _timeZone;
+ private string? _culture;
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ [Parameter]
+ public UserKey? UserKey { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Users.GetMeAsync();
+ }
+ else
+ {
+ result = await UAuthClient.Users.GetProfileAsync(UserKey.Value);
+ }
+
+ if (result.IsSuccess && result.Value is not null)
+ {
+ var p = result.Value;
+
+ _firstName = p.FirstName;
+ _lastName = p.LastName;
+ _displayName = p.DisplayName;
+
+ _gender = p.Gender;
+ _birthDate = p.BirthDate?.ToDateTime(TimeOnly.MinValue);
+ _bio = p.Bio;
+
+ _language = p.Language;
+ _timeZone = p.TimeZone;
+ _culture = p.Culture;
+ }
+ }
+
+ private async Task SaveAsync()
+ {
+ if (AuthState is null || AuthState.Identity is null)
+ {
+ Snackbar.Add("No AuthState found.", Severity.Error);
+ return;
+ }
+
+ if (_form is not null)
+ {
+ await _form.Validate();
+ if (!_form.IsValid)
+ return;
+ }
+
+ var request = new UpdateProfileRequest
+ {
+ FirstName = _firstName,
+ LastName = _lastName,
+ DisplayName = _displayName,
+ BirthDate = _birthDate.HasValue ? DateOnly.FromDateTime(_birthDate.Value) : null,
+ Gender = _gender,
+ Bio = _bio,
+ Language = _language,
+ TimeZone = _timeZone,
+ Culture = _culture
+ };
+
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Users.UpdateMeAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Users.UpdateProfileAsync(UserKey.Value, request);
+ }
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Profile updated", Severity.Success);
+ MudDialog.Close(DialogResult.Ok(true));
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "Failed to update profile", Severity.Error);
+ }
+ }
+
+ private void Cancel() => MudDialog.Cancel();
+}
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor
new file mode 100644
index 00000000..1e909a9f
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor
@@ -0,0 +1,75 @@
+@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
+ }
+
+
+
+
+ Cancel
+ OK
+
+
+
+@code {
+ private bool _resetRequested = false;
+ private string? _resetCode;
+ private string? _identifier;
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ private async Task RequestResetAsync()
+ {
+ var request = new BeginCredentialResetRequest
+ {
+ CredentialType = CredentialType.Password,
+ ResetCodeType = ResetCodeType.Code,
+ Identifier = _identifier ?? string.Empty
+ };
+
+ var result = await UAuthClient.Credentials.BeginResetMyAsync(request);
+ if (!result.IsSuccess || result.Value is null)
+ {
+ Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Failed to request credential reset.", Severity.Error);
+ return;
+ }
+
+ _resetCode = result.Value.Token;
+ _resetRequested = true;
+ }
+
+ private void Submit() => MudDialog.Close(DialogResult.Ok(true));
+
+ private void Cancel() => MudDialog.Cancel();
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor
new file mode 100644
index 00000000..0a44b8b9
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor
@@ -0,0 +1,238 @@
+@using CodeBeam.UltimateAuth.Authorization.Contracts
+@using CodeBeam.UltimateAuth.Core.Contracts
+@inject IUAuthClient UAuthClient
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+
+
+
+
+ Role Management
+ Manage system roles
+
+
+
+
+
+
+
+ Roles
+
+
+
+
+
+
+
+
+
+
+ @GetPermissionCount(context.Item)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
+ Close
+
+
+
+@code {
+ private MudDataGrid? _grid;
+ private bool _loading;
+ private string? _newRoleName;
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ private async Task> LoadServerData(GridState state, CancellationToken ct)
+ {
+ var sort = state.SortDefinitions?.FirstOrDefault();
+
+ var req = new RoleQuery
+ {
+ PageNumber = state.Page + 1,
+ PageSize = state.PageSize,
+ SortBy = sort?.SortBy,
+ Descending = sort?.Descending ?? false
+ };
+
+ var res = await UAuthClient.Authorization.QueryRolesAsync(req);
+
+ if (!res.IsSuccess || res.Value == null)
+ {
+ Snackbar.Add(res.Problem?.Title ?? "Failed", Severity.Error);
+
+ return new GridData
+ {
+ Items = Array.Empty(),
+ TotalItems = 0
+ };
+ }
+
+ return new GridData
+ {
+ Items = res.Value.Items,
+ TotalItems = res.Value.TotalCount
+ };
+ }
+
+ private async Task CommittedItemChanges(RoleInfo role)
+ {
+ var req = new RenameRoleRequest
+ {
+ Name = role.Name
+ };
+
+ var result = await UAuthClient.Authorization.RenameRoleAsync(role.Id, req);
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Role renamed", Severity.Success);
+ }
+ else
+ {
+ Snackbar.Add(result.Problem?.Title ?? "Rename failed", Severity.Error);
+ }
+
+ await ReloadAsync();
+ return DataGridEditFormAction.Close;
+ }
+
+ private async Task CreateRole()
+ {
+ if (string.IsNullOrWhiteSpace(_newRoleName))
+ {
+ Snackbar.Add("Role name required", Severity.Warning);
+ return;
+ }
+
+ var req = new CreateRoleRequest
+ {
+ Name = _newRoleName
+ };
+
+ var res = await UAuthClient.Authorization.CreateRoleAsync(req);
+
+ if (res.IsSuccess)
+ {
+ Snackbar.Add("Role created", Severity.Success);
+ await ReloadAsync();
+ }
+ else
+ {
+ Snackbar.Add(res.Problem?.Title ?? "Create failed", Severity.Error);
+ }
+ }
+
+ private async Task DeleteRole(RoleId roleId)
+ {
+ var confirm = await DialogService.ShowMessageBoxAsync(
+ "Delete role",
+ "Are you sure?",
+ yesText: "Delete",
+ cancelText: "Cancel",
+ options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass="uauth-blur-slight" });
+
+ if (confirm != true)
+ return;
+
+ var req = new DeleteRoleRequest();
+ var result = await UAuthClient.Authorization.DeleteRoleAsync(roleId, req);
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add($"Role deleted, assignments removed from {result.Value?.RemovedAssignments.ToString() ?? "unknown"} users.", Severity.Success);
+ await ReloadAsync();
+ }
+ else
+ {
+ Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Deletion failed.", Severity.Error);
+ }
+ }
+
+ private async Task EditPermissions(RoleInfo role)
+ {
+ var dialog = await DialogService.ShowAsync(
+ "Edit Permissions",
+ new DialogParameters
+ {
+ { nameof(PermissionDialog.Role), role }
+ },
+ new DialogOptions
+ {
+ CloseButton = true,
+ MaxWidth = MaxWidth.Large,
+ FullWidth = true
+ });
+
+ var result = await dialog.Result;
+ await ReloadAsync();
+ }
+
+ private async Task ReloadAsync()
+ {
+ _loading = true;
+ await Task.Delay(300);
+ if (_grid is null)
+ return;
+
+ await _grid.ReloadServerData();
+ _loading = false;
+ }
+
+ private int GetPermissionCount(RoleInfo role)
+ {
+ var expanded = PermissionExpander.Expand(role.Permissions, UAuthPermissionCatalog.GetAdminPermissions());
+ return expanded.Count;
+ }
+
+ private void Cancel() => MudDialog.Cancel();
+
+}
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor
new file mode 100644
index 00000000..c51e9987
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor
@@ -0,0 +1,492 @@
+@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 (_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
+
+
+
+
+
+
+
+
+ }
+
+
+ Cancel
+
+
+
+@code {
+ private MudDataGrid? _grid;
+ private bool _loading = false;
+ private bool _reloadQueued;
+ private SessionChainDetailDto? _chainDetail;
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ [Parameter]
+ public UserKey? UserKey { get; set; }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+
+ if (firstRender)
+ {
+ var result = await UAuthClient.Sessions.GetMyChainsAsync();
+ if (result != null && result.IsSuccess && result.Value != null)
+ {
+ await ReloadAsync();
+ StateHasChanged();
+ }
+ }
+ }
+
+ private async Task> LoadServerData(GridState state, CancellationToken ct)
+ {
+ var sort = state.SortDefinitions?.FirstOrDefault();
+
+ var req = new PageRequest
+ {
+ PageNumber = state.Page + 1,
+ PageSize = state.PageSize,
+ SortBy = sort?.SortBy,
+ Descending = sort?.Descending ?? false
+ };
+
+ UAuthResult> res;
+
+ if (UserKey is null)
+ {
+ res = await UAuthClient.Sessions.GetMyChainsAsync(req);
+ }
+ else
+ {
+ res = await UAuthClient.Sessions.GetUserChainsAsync(UserKey.Value, req);
+ }
+
+ if (!res.IsSuccess || res.Value is null)
+ {
+ Snackbar.Add(res.Problem?.Title ?? "Failed", Severity.Error);
+
+ return new GridData
+ {
+ Items = Array.Empty(),
+ TotalItems = 0
+ };
+ }
+
+ return new GridData
+ {
+ Items = res.Value.Items,
+ TotalItems = res.Value.TotalCount
+ };
+ }
+
+ private async Task ReloadAsync()
+ {
+ if (_loading)
+ {
+ _reloadQueued = true;
+ return;
+ }
+
+ if (_grid is null)
+ return;
+
+ _loading = true;
+ await InvokeAsync(StateHasChanged);
+ await Task.Delay(300);
+
+ try
+ {
+ await _grid.ReloadServerData();
+ }
+ finally
+ {
+ _loading = false;
+
+ if (_reloadQueued)
+ {
+ _reloadQueued = false;
+ await ReloadAsync();
+ }
+
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
+ private async Task LogoutAllAsync()
+ {
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Flows.LogoutAllDevicesSelfAsync();
+ }
+ else
+ {
+ result = await UAuthClient.Flows.LogoutAllDevicesAdminAsync(UserKey.Value);
+ }
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Logged out of all devices.", Severity.Success);
+ if (UserKey is null)
+ Nav.NavigateTo("/login");
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task LogoutOthersAsync()
+ {
+ var result = await UAuthClient.Flows.LogoutOtherDevicesSelfAsync();
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Logged out of other devices.", Severity.Success);
+ }
+ else
+ {
+ Snackbar.Add(result?.GetErrorText ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task LogoutDeviceAsync(SessionChainId chainId)
+ {
+ LogoutDeviceRequest request = new() { ChainId = chainId };
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Flows.LogoutDeviceSelfAsync(request);
+ }
+ else
+ {
+ result = await UAuthClient.Flows.LogoutDeviceAdminAsync(UserKey.Value, request);
+ }
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Logged out of device.", Severity.Success);
+ if (result?.Value?.CurrentChain == true)
+ {
+ Nav.NavigateTo("/login");
+ return;
+ }
+ await ReloadAsync();
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task RevokeAllAsync()
+ {
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Sessions.RevokeAllMyChainsAsync();
+ }
+ else
+ {
+ result = await UAuthClient.Sessions.RevokeAllUserChainsAsync(UserKey.Value);
+ }
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Logged out of all devices.", Severity.Success);
+
+ if (UserKey is null)
+ Nav.NavigateTo("/login");
+ }
+ else
+ {
+ Snackbar.Add(result?.GetErrorText ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task RevokeOthersAsync()
+ {
+ var result = await UAuthClient.Sessions.RevokeMyOtherChainsAsync();
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Revoked all other devices.", Severity.Success);
+ await ReloadAsync();
+ }
+ else
+ {
+ Snackbar.Add(result?.GetErrorText ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task RevokeChainAsync(SessionChainId chainId)
+ {
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Sessions.RevokeMyChainAsync(chainId);
+ }
+ else
+ {
+ result = await UAuthClient.Sessions.RevokeUserChainAsync(UserKey.Value, chainId);
+ }
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Device revoked successfully.", Severity.Success);
+
+ if (result?.Value?.CurrentChain == true)
+ {
+ Nav.NavigateTo("/login");
+ return;
+ }
+ await ReloadAsync();
+ }
+ else
+ {
+ Snackbar.Add(result?.GetErrorText ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task ShowChainDetailsAsync(SessionChainId chainId)
+ {
+ UAuthResult result;
+
+ if (UserKey is null)
+ {
+ result = await UAuthClient.Sessions.GetMyChainDetailAsync(chainId);
+ }
+ else
+ {
+ result = await UAuthClient.Sessions.GetUserChainDetailAsync(UserKey.Value, chainId);
+ }
+
+ if (result.IsSuccess)
+ {
+ _chainDetail = result.Value;
+ }
+ else
+ {
+ Snackbar.Add(result?.GetErrorText ?? "Failed to fetch chain details.", Severity.Error);
+ _chainDetail = null;
+ }
+ }
+
+ private void ClearDetail()
+ {
+ _chainDetail = null;
+ }
+
+ private void Cancel() => MudDialog.Cancel();
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor
new file mode 100644
index 00000000..92aeaa8e
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserDetailDialog.razor
@@ -0,0 +1,165 @@
+@using CodeBeam.UltimateAuth.Core.Contracts
+@using CodeBeam.UltimateAuth.Sample.BlazorServer.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
+
+
+
+@code {
+ private UserView? _user;
+ private UserStatus _status;
+
+ [CascadingParameter]
+ IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ [Parameter]
+ public UserKey UserKey { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ await base.OnInitializedAsync();
+ var result = await UAuthClient.Users.GetProfileAsync(UserKey);
+
+ if (result.IsSuccess)
+ {
+ _user = result.Value;
+ _status = _user?.Status ?? UserStatus.Unknown;
+ }
+ }
+
+ private async Task OpenSessions()
+ {
+ await DialogService.ShowAsync("Session Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions());
+ }
+
+ private async Task OpenProfile()
+ {
+ await DialogService.ShowAsync("Profile Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions());
+ }
+
+ private async Task OpenIdentifiers()
+ {
+ await DialogService.ShowAsync("Identifier Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions());
+ }
+
+ private async Task OpenCredentials()
+ {
+ await DialogService.ShowAsync("Credentials", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions());
+ }
+
+ private async Task OpenRoles()
+ {
+ await DialogService.ShowAsync("Roles", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions());
+ }
+
+ private async Task ChangeStatusAsync()
+ {
+ if (_user is null)
+ return;
+
+ ChangeUserStatusAdminRequest request = new()
+ {
+ NewStatus = _status
+ };
+
+ var result = await UAuthClient.Users.ChangeStatusAdminAsync(_user.UserKey, request);
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("User status updated", Severity.Success);
+ _user = _user with { Status = _status };
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "Failed", Severity.Error);
+ }
+ }
+
+ private Color GetStatusColor(UserStatus? status)
+ {
+ return status switch
+ {
+ UserStatus.Active => Color.Success,
+ UserStatus.Suspended => Color.Warning,
+ UserStatus.Disabled => Color.Error,
+ _ => Color.Default
+ };
+ }
+
+ private void Close()
+ {
+ MudDialog.Close();
+ }
+}
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor
new file mode 100644
index 00000000..9ae32dfa
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UserRoleDialog.razor
@@ -0,0 +1,154 @@
+@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
+
+
+
+@code {
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ [Parameter]
+ public UserKey UserKey { get; set; } = default!;
+
+ private List _roles = new();
+ private List _allRoles = new();
+
+ private string? _selectedRole;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadRoles();
+ }
+
+ private async Task LoadRoles()
+ {
+ var userRoles = await UAuthClient.Authorization.GetUserRolesAsync(UserKey);
+
+ if (userRoles.IsSuccess && userRoles.Value != null)
+ _roles = userRoles.Value.Roles.Items.Select(x => x.Name).ToList();
+
+ var roles = await UAuthClient.Authorization.QueryRolesAsync(new RoleQuery
+ {
+ PageNumber = 1,
+ PageSize = 200
+ });
+
+ if (roles.IsSuccess && roles.Value != null)
+ _allRoles = roles.Value.Items.ToList();
+ }
+
+ private async Task AddRole()
+ {
+ if (string.IsNullOrWhiteSpace(_selectedRole))
+ return;
+
+ var result = await UAuthClient.Authorization.AssignRoleToUserAsync(UserKey, _selectedRole);
+
+ if (result.IsSuccess)
+ {
+ _roles.Add(_selectedRole);
+ Snackbar.Add("Role assigned", Severity.Success);
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "Failed", Severity.Error);
+ }
+
+ _selectedRole = null;
+ }
+
+ private async Task RemoveRole(string role)
+ {
+ var confirm = await DialogService.ShowMessageBoxAsync(
+ "Remove Role",
+ $"Remove {role} from user?",
+ yesText: "Remove",
+ noText: "Cancel",
+ options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" });
+
+ if (confirm != true)
+ {
+ Snackbar.Add("Role remove process cancelled.", Severity.Info);
+ return;
+ }
+
+ if (role == "Admin")
+ {
+ var confirm2 = await DialogService.ShowMessageBoxAsync(
+ "Are You Sure",
+ "You are going to remove admin role. This action may cause the application unuseable.",
+ yesText: "Remove",
+ noText: "Cancel",
+ options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" });
+
+ if (confirm2 != true)
+ {
+ Snackbar.Add("Role remove process cancelled.", Severity.Info);
+ return;
+ }
+ }
+
+ var result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(UserKey, role);
+
+ if (result.IsSuccess)
+ {
+ _roles.Remove(role);
+ Snackbar.Add("Role removed", Severity.Success);
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "Failed", Severity.Error);
+ }
+ }
+
+ private void Close() => MudDialog.Close();
+}
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor
new file mode 100644
index 00000000..39f3c5e1
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor
@@ -0,0 +1,251 @@
+@using CodeBeam.UltimateAuth.Core.Contracts
+@using CodeBeam.UltimateAuth.Sample.BlazorServer.Common
+@using CodeBeam.UltimateAuth.Users.Contracts
+@inject IUAuthClient UAuthClient
+@inject IDialogService DialogService
+@inject ISnackbar Snackbar
+
+
+
+ User Management
+ Browse, create and manage users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Users
+
+ New User
+
+
+
+
+
+
+
+
+
+
+ @context.Item.Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Id
+ @context.Item.UserKey.Value
+
+
+
+ Created At
+ @context.Item.CreatedAt
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+
+
+@code {
+ private MudDataGrid? _grid;
+ private bool _loading;
+ private string? _search;
+ private bool _reloadQueued;
+ private UserStatus? _statusFilter;
+
+ [CascadingParameter]
+ IMudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter]
+ public UAuthState AuthState { get; set; } = default!;
+
+ private async Task> LoadUsers(GridState state, CancellationToken ct)
+ {
+ var sort = state.SortDefinitions?.FirstOrDefault();
+
+ var req = new UserQuery
+ {
+ PageNumber = state.Page + 1,
+ PageSize = state.PageSize,
+ Search = _search,
+ Status = _statusFilter,
+ SortBy = sort?.SortBy,
+ Descending = sort?.Descending ?? false
+ };
+
+ var res = await UAuthClient.Users.QueryUsersAsync(req);
+
+ if (!res.IsSuccess || res.Value == null)
+ {
+ Snackbar.Add(res.GetErrorText ?? "Failed to load users", Severity.Error);
+
+ return new GridData
+ {
+ Items = Array.Empty(),
+ TotalItems = 0
+ };
+ }
+
+ return new GridData
+ {
+ Items = res.Value.Items,
+ TotalItems = res.Value.TotalCount
+ };
+ }
+
+ private async Task ReloadAsync()
+ {
+ if (_loading)
+ {
+ _reloadQueued = true;
+ return;
+ }
+
+ if (_grid is null)
+ return;
+
+ _loading = true;
+ await InvokeAsync(StateHasChanged);
+
+ try
+ {
+ await _grid.ReloadServerData();
+ }
+ finally
+ {
+ _loading = false;
+
+ if (_reloadQueued)
+ {
+ _reloadQueued = false;
+ await ReloadAsync();
+ }
+
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
+ private async Task OnStatusChanged(UserStatus? status)
+ {
+ _statusFilter = status;
+ await ReloadAsync();
+ }
+
+ private async Task OpenUser(UserKey userKey)
+ {
+ var dialog = await DialogService.ShowAsync("User", UAuthDialog.GetDialogParameters(AuthState, userKey), UAuthDialog.GetDialogOptions());
+ await dialog.Result;
+ await ReloadAsync();
+ }
+
+ private async Task OpenCreateUser()
+ {
+ var dialog = await DialogService.ShowAsync(
+ "Create User",
+ new DialogOptions
+ {
+ MaxWidth = MaxWidth.Small,
+ FullWidth = true,
+ CloseButton = true
+ });
+
+ var result = await dialog.Result;
+
+ if (result?.Canceled == false)
+ await ReloadAsync();
+ }
+
+ private async Task DeleteUserAsync(UserSummary user)
+ {
+ var confirm = await DialogService.ShowMessageBoxAsync(
+ title: "Delete user",
+ markupMessage: (MarkupString)$"""
+ Are you sure you want to delete {user.DisplayName ?? user.UserName ?? user.PrimaryEmail ?? user.UserKey}?
+
+ This operation is intended for admin usage.
+ """,
+ yesText: "Delete",
+ cancelText: "Cancel",
+ options: new DialogOptions
+ {
+ MaxWidth = MaxWidth.Medium,
+ FullWidth = true,
+ BackgroundClass = "uauth-blur-slight"
+ });
+
+ if (confirm != true)
+ return;
+
+ var req = new DeleteUserRequest
+ {
+ Mode = DeleteMode.Soft
+ };
+
+ var result = await UAuthClient.Users.DeleteUserAsync(UserKey.Parse(user.UserKey, null), req);
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("User deleted successfully", Severity.Success);
+ await ReloadAsync();
+ }
+ else
+ {
+ Snackbar.Add(result.GetErrorText ?? "Failed to delete user", Severity.Error);
+ }
+ }
+
+ private static Color GetStatusColor(UserStatus status)
+ {
+ return status switch
+ {
+ UserStatus.Active => Color.Success,
+ UserStatus.SelfSuspended => Color.Warning,
+ UserStatus.Suspended => Color.Warning,
+ UserStatus.Disabled => Color.Error,
+ _ => Color.Default
+ };
+ }
+
+ private void Close()
+ {
+ MudDialog.Close();
+ }
+}
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor
index 336f6537..7239bf28 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor
@@ -4,11 +4,11 @@
@inject NavigationManager Nav
-
+
- Nav.NavigateTo("/home"))">UltimateAuth
-
- Blazor Server Sample
+ Nav.NavigateTo("/home", true))">UltimateAuth
+
+ Blazor Server Sample
@@ -34,6 +34,7 @@
+
@if (state.Identity?.SessionState is not null && state.Identity.SessionState != SessionState.Active)
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor.cs
index 8ee2d479..47d68df7 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Layout/MainLayout.razor.cs
@@ -1,5 +1,8 @@
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.BlazorServer.Infrastructure;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -56,6 +59,57 @@ private void HandleSignInClick()
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);
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor
new file mode 100644
index 00000000..b059ee89
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor
@@ -0,0 +1,2 @@
+@page "/authorized-test"
+@attribute [Authorize]
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor
index f1008c9d..0518403b 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor
@@ -8,20 +8,139 @@
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using System.Security.Claims
+@using CodeBeam.UltimateAuth.Core.Contracts
+@using CodeBeam.UltimateAuth.Core.Defaults
+@using CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Custom
+
+@if (AuthState?.Identity?.UserStatus == UserStatus.SelfSuspended)
+{
+
+
+
+ Your account is suspended. Please active it before continue.
+
+
+
+ Set Active
+ 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
+
+
+
+
+ }
+
+
+
+
+
+
+
- @(AuthState?.Identity?.DisplayName?.Substring(0, 2))
+
+ @((AuthState?.Identity?.DisplayName ?? "?").Substring(0, Math.Min(2, (AuthState?.Identity?.DisplayName ?? "?").Length)))
+
@AuthState?.Identity?.DisplayName
- @foreach (var role in AuthState.Claims.Roles)
+ @foreach (var role in AuthState?.Claims?.Roles ?? Enumerable.Empty())
{
@role
@@ -66,7 +185,7 @@
Authenticated
- @(AuthState.IsAuthenticated ? "Yes" : "No")
+ @(AuthState?.IsAuthenticated == true ? "Yes" : "No")
@@ -134,6 +253,7 @@
Last Validated At
+ @* TODO: Validation call should update last validated at *@
@FormatLocalTime(AuthState?.LastValidatedAt)
@@ -186,202 +306,121 @@
-
-
-
-
- Session
+
+
+
+
+ @GetHealthText()
+
-
-
-
- Validate
-
-
+ Lifecycle
-
-
- Manual Refresh
-
-
-
-
-
- Logout
-
-
-
+
-
-
- Account
-
- Manage Identifiers
-
-
-
- Change Password
-
-
-
-
-
- Admin
-
-
-
-
-
-
-
- @if (AuthState.IsInRole("Admin") || _showAdminPreview)
+
+
+ Started
+ @Diagnostics.StartCount
+
+ @if (Diagnostics.StartedAt is not null)
{
-
-
-
- Add User
-
-
-
-
- Assign Role
-
-
-
+
+
+
+ @FormatRelative(Diagnostics.StartedAt)
+
+
}
-
- @if (_showAdminPreview)
+
+
+
+
+ Stopped
+ @Diagnostics.StopCount
+
+
+
+
+
+ Terminated
+ @Diagnostics.TerminatedCount
+
+ @if (Diagnostics.TerminatedAt is not null)
{
-
- Admin operations are shown for preview. Sign in as an Admin to execute them.
-
+
+
+
+
+ @FormatRelative(Diagnostics.TerminatedAt)
+
+
+
}
+
-
-
-
-
-
- @GetHealthText()
-
+
- Lifecycle
+
+ Refresh Metrics
+
-
+
-
-
- Started
- @Diagnostics.StartCount
+
+
+ Total Attempts
+ @Diagnostics.RefreshAttemptCount
- @if (Diagnostics.StartedAt is not null)
- {
-
-
-
-
- @FormatRelative(Diagnostics.StartedAt)
-
-
-
- }
-
-
- Stopped
-
- @Diagnostics.StopCount
-
+
+
+
+ Success
+
+ @Diagnostics.RefreshSuccessCount
-
-
- Terminated
- @Diagnostics.TerminatedCount
+
+
+ Automatic
+ @Diagnostics.AutomaticRefreshCount
- @if (Diagnostics.TerminatedAt is not null)
- {
-
-
-
-
- @FormatRelative(Diagnostics.TerminatedAt)
-
-
-
- }
-
-
-
- Refresh Metrics
-
-
-
-
-
-
- Total Attempts
- @Diagnostics.RefreshAttemptCount
-
-
-
-
-
-
- Success
-
- @Diagnostics.RefreshSuccessCount
-
-
-
-
-
- Automatic
- @Diagnostics.AutomaticRefreshCount
-
-
-
-
-
- Manual
- @Diagnostics.ManualRefreshCount
-
-
+
+
+ Manual
+ @Diagnostics.ManualRefreshCount
+
+
-
-
- Touched
- @Diagnostics.RefreshTouchedCount
-
-
+
+
+ Touched
+ @Diagnostics.RefreshTouchedCount
+
+
-
-
- No-Op
- @Diagnostics.RefreshNoOpCount
-
-
+
+
+ No-Op
+ @Diagnostics.RefreshNoOpCount
+
+
-
-
- Reauth Required
- @Diagnostics.RefreshReauthRequiredCount
-
-
-
+
+
+ Reauth Required
+ @Diagnostics.RefreshReauthRequiredCount
+
+
-
-
+
+
+
-
\ No newline at end of file
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs
index 38a6f1cc..72aab311 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs
@@ -1,8 +1,11 @@
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.BlazorServer.Common;
using CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Dialogs;
+using CodeBeam.UltimateAuth.Users.Contracts;
using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor;
using System.Security.Claims;
@@ -60,6 +63,11 @@ private async Task Validate()
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
@@ -98,10 +106,6 @@ private async Task Validate()
}
}
- private Task CreateUser() => Task.CompletedTask;
- private Task AssignRole() => Task.CompletedTask;
- private Task ChangePassword() => Task.CompletedTask;
-
private Color GetHealthColor()
{
if (Diagnostics.RefreshReauthRequiredCount > 0)
@@ -151,30 +155,76 @@ private string GetHealthText()
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());
+ }
- await DialogService.ShowAsync("Manage Identifiers", GetDialogParameters(), GetDialogOptions());
+ private async Task OpenSessionDialog()
+ {
+ await DialogService.ShowAsync("Manage Sessions", GetDialogParameters(), UAuthDialog.GetDialogOptions());
}
- private DialogOptions GetDialogOptions()
+ private async Task OpenCredentialDialog()
{
- return new DialogOptions
- {
- MaxWidth = MaxWidth.Medium,
- FullWidth = true,
- CloseButton = true
- };
+ await DialogService.ShowAsync("Session Diagnostics", GetDialogParameters(), UAuthDialog.GetDialogOptions());
+ }
+
+ private async Task OpenAccountStatusDialog()
+ {
+ await DialogService.ShowAsync("Manage Account", GetDialogParameters(), UAuthDialog.GetDialogOptions());
+ }
+
+ private async Task OpenUserDialog()
+ {
+ await DialogService.ShowAsync("User Management", GetDialogParameters(), UAuthDialog.GetDialogOptions(MaxWidth.Large));
+ }
+
+ private async Task OpenRoleDialog()
+ {
+ await DialogService.ShowAsync("Role Management", GetDialogParameters(), UAuthDialog.GetDialogOptions());
}
private DialogParameters GetDialogParameters()
- {
+ {
return new DialogParameters
{
["AuthState"] = AuthState
};
}
+ private async Task SetAccountActiveAsync()
+ {
+ ChangeUserStatusSelfRequest request = new() { NewStatus = SelfUserStatus.Active };
+ var result = await UAuthClient.Users.ChangeStatusSelfAsync(request);
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Account activated successfully.", Severity.Success);
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Activation failed.", Severity.Error);
+ }
+ }
+
+ private string? _roles = "Admin";
+ private void RefreshHiddenState()
+ {
+ if (_roles == "Admin")
+ {
+ _roles = "User";
+ return;
+ }
+
+ _roles = "Admin";
+ }
+
public override void Dispose()
{
base.Dispose();
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/LandingPage.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/LandingPage.razor.cs
index 41a7e106..ab16ba5e 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/LandingPage.razor.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/LandingPage.razor.cs
@@ -1,4 +1,4 @@
-using CodeBeam.UltimateAuth.Core.Constants;
+using CodeBeam.UltimateAuth.Core.Defaults;
namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Pages;
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor
index 1db4091f..7687854b 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor
@@ -7,9 +7,10 @@
@inject ISnackbar Snackbar
@inject IUAuthClientProductInfoProvider ClientProductInfoProvider
@inject IDeviceIdProvider DeviceIdProvider
+@inject IDialogService DialogService
-
+
@@ -72,7 +73,7 @@
-
+
@@ -109,7 +110,14 @@
Programmatic Login
- Login programmatically as admin/admin.
+ Login programmatically as admin/admin.
+
+
+
+
+
+ Forgot Password
+ Don't have an account? SignUp
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs
index 757ca466..f4747658 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor.cs
@@ -2,6 +2,7 @@
using CodeBeam.UltimateAuth.Client.Runtime;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Dialogs;
using MudBlazor;
namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Pages;
@@ -84,9 +85,8 @@ private async Task ProgrammaticLogin()
{
Identifier = "admin",
Secret = "admin",
- Device = DeviceContext.FromDeviceId(deviceId),
};
- await UAuthClient.Flows.LoginAsync(request, "/home");
+ await UAuthClient.Flows.LoginAsync(request, ReturnUrl ?? "/home");
}
private async void StartCountdown()
@@ -159,6 +159,29 @@ private void UpdateRemaining()
}
}
+ 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();
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor
index c4b58a61..2c0e9b77 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/NotAuthorized.razor
@@ -1,6 +1,6 @@
@inject NavigationManager Nav
-
+
@@ -24,4 +24,4 @@
-
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor
new file mode 100644
index 00000000..e32cc79c
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor
@@ -0,0 +1,54 @@
+@page "/register"
+@inherits UAuthFlowPageBase
+
+@implements IDisposable
+@inject IUAuthClient UAuthClient
+@inject ISnackbar Snackbar
+@inject IUAuthClientProductInfoProvider ClientProductInfoProvider
+@inject IDeviceIdProvider DeviceIdProvider
+@inject IDialogService DialogService
+
+
+
+
+
+
+
+
+
+
+ UltimateAuth
+ The Modern Unified Auth Framework for .NET — Reimagined.
+
+
+
+
+ @_productInfo?.ProductName v @_productInfo?.Version
+ Client Profile: @_productInfo?.ClientProfile.ToString()
+ @_productInfo?.FrameworkDescription
+
+
+
+
+
+
+
+
+
+
+
+ Register
+
+
+
+
+
+
+ Sign Up
+
+
+
+
+
+
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs
new file mode 100644
index 00000000..82645070
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor.cs
@@ -0,0 +1,45 @@
+using CodeBeam.UltimateAuth.Client.Runtime;
+using CodeBeam.UltimateAuth.Users.Contracts;
+using MudBlazor;
+
+namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Pages;
+
+public partial class Register
+{
+ private string? _username;
+ private string? _password;
+ private string? _passwordCheck;
+ private string? _email;
+ private UAuthClientProductInfo? _productInfo;
+ private MudForm _form = null!;
+
+ protected override async Task OnInitializedAsync()
+ {
+ _productInfo = ClientProductInfoProvider.Get();
+ }
+
+ private async Task HandleRegisterAsync()
+ {
+ await _form.Validate();
+
+ if (!_form.IsValid)
+ return;
+
+ var request = new CreateUserRequest
+ {
+ UserName = _username,
+ Password = _password,
+ Email = _email,
+ };
+
+ var result = await UAuthClient.Users.CreateAsync(request);
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("User created succesfully.", Severity.Success);
+ }
+ else
+ {
+ Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Failed to create user.", Severity.Error);
+ }
+ }
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor
new file mode 100644
index 00000000..753878b8
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor
@@ -0,0 +1,18 @@
+@page "/reset"
+@inherits UAuthFlowPageBase
+
+@inject IUAuthClient UAuthClient
+@inject ISnackbar Snackbar
+
+
+
+
+
+
+
+
+ Change Password
+
+
+
+
\ No newline at end of file
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor.cs
new file mode 100644
index 00000000..fd66181e
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/ResetCredential.razor.cs
@@ -0,0 +1,49 @@
+using CodeBeam.UltimateAuth.Credentials.Contracts;
+using MudBlazor;
+
+namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Pages;
+
+public partial class ResetCredential
+{
+ private MudForm _form = null!;
+ private string? _code;
+ private string? _newPassword;
+ private string? _newPasswordCheck;
+
+ private async Task ResetPasswordAsync()
+ {
+ await _form.Validate();
+ if (!_form.IsValid)
+ {
+ Snackbar.Add("Please fix the validation errors.", Severity.Error);
+ return;
+ }
+
+ if (_newPassword != _newPasswordCheck)
+ {
+ Snackbar.Add("Passwords do not match.", Severity.Error);
+ return;
+ }
+
+ var request = new CompleteCredentialResetRequest
+ {
+ ResetToken = _code,
+ NewSecret = _newPassword ?? string.Empty,
+ Identifier = Identifier // Coming from UAuthFlowPageBase automatically if begin reset is successful
+ };
+
+ var result = await UAuthClient.Credentials.CompleteResetMyAsync(request);
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Credential reset successfully. Please log in with your new password.", Severity.Success);
+ Nav.NavigateTo("/login");
+ }
+ else
+ {
+ Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Failed to reset credential. Please try again.", Severity.Error);
+ }
+ }
+
+ private string PasswordMatch(string arg) => _newPassword != arg ? "Passwords don't match" : string.Empty;
+}
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
index 19af773b..dee4c413 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
@@ -1,22 +1,24 @@
+using CodeBeam.UltimateAuth.Authentication.InMemory;
using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions;
using CodeBeam.UltimateAuth.Authorization.Reference.Extensions;
+using CodeBeam.UltimateAuth.Client;
using CodeBeam.UltimateAuth.Client.Extensions;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Infrastructure;
using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions;
using CodeBeam.UltimateAuth.Credentials.Reference;
using CodeBeam.UltimateAuth.Sample.BlazorServer.Components;
+using CodeBeam.UltimateAuth.Sample.BlazorServer.Infrastructure;
using CodeBeam.UltimateAuth.Security.Argon2;
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Sessions.InMemory;
using CodeBeam.UltimateAuth.Tokens.InMemory;
using CodeBeam.UltimateAuth.Users.InMemory.Extensions;
using CodeBeam.UltimateAuth.Users.Reference.Extensions;
-using CodeBeam.UltimateAuth.Client;
+using Microsoft.AspNetCore.HttpOverrides;
using MudBlazor.Services;
using MudExtensions.Services;
using Scalar.AspNetCore;
-using CodeBeam.UltimateAuth.Sample.BlazorServer.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
@@ -45,7 +47,7 @@
//o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32);
o.Login.MaxFailedAttempts = 2;
o.Login.LockoutDuration = TimeSpan.FromSeconds(10);
- o.UserIdentifiers.AllowMultipleUsernames = true;
+ o.Identifiers.AllowMultipleUsernames = true;
})
.AddUltimateAuthUsersInMemory()
.AddUltimateAuthUsersReference()
@@ -55,16 +57,25 @@
.AddUltimateAuthAuthorizationReference()
.AddUltimateAuthInMemorySessions()
.AddUltimateAuthInMemoryTokens()
+ .AddUltimateAuthInMemoryAuthenticationSecurity()
.AddUltimateAuthArgon2();
builder.Services.AddUltimateAuthClient(o =>
{
//o.AutoRefresh.Interval = TimeSpan.FromSeconds(5);
o.Reauth.Behavior = ReauthBehavior.RaiseEvent;
+ //o.UAuthStateRefreshMode = UAuthStateRefreshMode.Validate;
});
builder.Services.AddScoped();
+builder.Services.Configure(options =>
+{
+ options.ForwardedHeaders =
+ ForwardedHeaders.XForwardedFor |
+ ForwardedHeaders.XForwardedProto;
+});
+
var app = builder.Build();
if (!app.Environment.IsDevelopment())
@@ -82,6 +93,8 @@
await seedRunner.RunAsync(null);
}
+app.UseForwardedHeaders();
+
app.UseHttpsRedirection();
app.UseStaticFiles();
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/wwwroot/app.css b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/wwwroot/app.css
index 202b506b..2b9a4745 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/wwwroot/app.css
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/wwwroot/app.css
@@ -50,12 +50,6 @@ h1:focus {
border-color: #929292;
}
-.uauth-page {
- height: calc(100vh - var(--mud-appbar-height));
- width: 100vw;
- margin-top: 64px;
-}
-
.uauth-stack {
min-height: 60vh;
max-height: calc(100vh - var(--mud-appbar-height));
@@ -68,7 +62,7 @@ h1:focus {
}
.uauth-login-paper {
- min-height: 60vh;
+ min-height: 70vh;
}
.uauth-login-paper.mud-theme-primary {
@@ -81,13 +75,69 @@ h1:focus {
}
.uauth-logo-slide {
- transition: transform 1s cubic-bezier(.4, 0, .2, 1);
-}
-
-.uauth-login-paper:hover .uauth-logo-slide {
- transform: translateY(200px) rotateY(360deg);
+ animation: uauth-logo-float 30s ease-in-out infinite;
}
.uauth-text-transform-none .mud-button {
text-transform: none;
}
+
+.uauth-dialog {
+ height: 68vh;
+ max-height: 68vh;
+ overflow: auto;
+}
+
+.text-secondary {
+ color: var(--mud-palette-text-secondary);
+}
+
+.uauth-blur {
+ backdrop-filter: blur(10px);
+}
+
+.uauth-blur-slight {
+ backdrop-filter: blur(4px);
+}
+
+@keyframes uauth-logo-float {
+ 0% {
+ transform: translateY(0) rotateY(0);
+ }
+
+ 10% {
+ transform: translateY(0) rotateY(0);
+ }
+
+ 15% {
+ transform: translateY(200px) rotateY(360deg);
+ }
+
+ 35% {
+ transform: translateY(200px) rotateY(360deg);
+ }
+
+ 40% {
+ transform: translateY(200px) rotateY(720deg);
+ }
+
+ 60% {
+ transform: translateY(200px) rotateY(720deg);
+ }
+
+ 65% {
+ transform: translateY(0) rotateY(360deg);
+ }
+
+ 85% {
+ transform: translateY(0) rotateY(360deg);
+ }
+
+ 90% {
+ transform: translateY(0) rotateY(0);
+ }
+
+ 100% {
+ transform: translateY(0) rotateY(0);
+ }
+}
diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs
index 55315ca7..6ecdf083 100644
--- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs
+++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs
@@ -41,12 +41,11 @@ private void OnDiagnosticsChanged()
private async Task ProgrammaticLogin()
{
- var device = await DeviceIdProvider.GetOrCreateAsync();
+ var deviceId = await DeviceIdProvider.GetOrCreateAsync();
var request = new LoginRequest
{
Identifier = "admin",
Secret = "admin",
- Device = DeviceContext.FromDeviceId(device),
};
await UAuthClient.Flows.LoginAsync(request);
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/IUAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/IUAuthStateManager.cs
similarity index 86%
rename from src/CodeBeam.UltimateAuth.Client/Authentication/IUAuthStateManager.cs
rename to src/CodeBeam.UltimateAuth.Client/AuthState/IUAuthStateManager.cs
index f30e0447..c5c28a5c 100644
--- a/src/CodeBeam.UltimateAuth.Client/Authentication/IUAuthStateManager.cs
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/IUAuthStateManager.cs
@@ -32,4 +32,9 @@ public interface IUAuthStateManager
/// Forces state to be cleared and re-validation required.
///
void MarkStale();
+
+ ///
+ /// Removes all authentication state information managed by the implementation.
+ ///
+ void Clear();
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthAuthenticationStateProvider.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthAuthenticationStateProvider.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Client/Authentication/UAuthAuthenticationStateProvider.cs
rename to src/CodeBeam.UltimateAuth.Client/AuthState/UAuthAuthenticationStateProvider.cs
diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthCascadingStateProvider.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthCascadingStateProvider.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Client/Authentication/UAuthCascadingStateProvider.cs
rename to src/CodeBeam.UltimateAuth.Client/AuthState/UAuthCascadingStateProvider.cs
diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs
similarity index 62%
rename from src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs
rename to src/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs
index 7b106dd0..cbc32fbe 100644
--- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs
@@ -1,6 +1,9 @@
-using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Authorization.Contracts;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Extensions;
+using CodeBeam.UltimateAuth.Users.Contracts;
using System.Security.Claims;
namespace CodeBeam.UltimateAuth.Client;
@@ -27,8 +30,10 @@ private UAuthState() { }
public event Action? Changed;
+ internal Action? RequestRender;
public bool IsAuthenticated => Identity is not null;
+ public bool NeedsValidation => IsAuthenticated && IsStale;
public static UAuthState Anonymous() => new();
@@ -37,12 +42,40 @@ internal void ApplySnapshot(AuthStateSnapshot snapshot, DateTimeOffset validated
Identity = snapshot.Identity;
Claims = snapshot.Claims;
+ _compiledPermissions = new CompiledPermissionSet(Claims.Permissions.Select(Permission.From));
+
IsStale = false;
LastValidatedAt = validatedAt;
Changed?.Invoke(UAuthStateChangeReason.Authenticated);
}
+ internal void UpdateProfile(UpdateProfileRequest req)
+ {
+ if (Identity is null)
+ return;
+
+ Identity = Identity with
+ {
+ DisplayName = req.DisplayName ?? Identity.DisplayName
+ };
+
+ Changed?.Invoke(UAuthStateChangeReason.Patched);
+ }
+
+ internal void UpdateUserStatus(ChangeUserStatusSelfRequest req)
+ {
+ if (Identity is null)
+ return;
+
+ Identity = Identity with
+ {
+ UserStatus = UserStatusMapper.ToUserStatus(req.NewStatus)
+ };
+
+ Changed?.Invoke(UAuthStateChangeReason.Patched);
+ }
+
internal void MarkValidated(DateTimeOffset now)
{
if (!IsAuthenticated)
@@ -63,6 +96,16 @@ internal void MarkStale()
Changed?.Invoke(UAuthStateChangeReason.MarkedStale);
}
+ public void Touch(bool updateState = true)
+ {
+ if (updateState)
+ {
+ IsStale = true;
+ }
+
+ RequestRender?.Invoke();
+ }
+
internal void Clear()
{
Identity = null;
@@ -75,7 +118,28 @@ internal void Clear()
public bool IsInRole(string role) => IsAuthenticated && Claims.IsInRole(role);
- public bool HasPermission(string permission) => IsAuthenticated && Claims.HasPermission(permission);
+ private CompiledPermissionSet? _compiledPermissions;
+ public bool HasPermission(string permission)
+ {
+ if (!IsAuthenticated)
+ return false;
+
+ if (Claims.HasPermission(permission))
+ return true;
+
+ return _compiledPermissions?.IsAllowed(permission) == true;
+ }
+
+ public bool HasAnyPermission(params string[] permissions)
+ {
+ foreach (var perm in permissions)
+ {
+ if (HasPermission(perm))
+ return true;
+ }
+
+ return false;
+ }
public bool HasClaim(string type, string value) => IsAuthenticated && Claims.HasValue(type, value);
@@ -84,7 +148,7 @@ internal void Clear()
///
/// Creates a ClaimsPrincipal view for ASP.NET / Blazor integration.
///
- public ClaimsPrincipal ToClaimsPrincipal(string authenticationType = "UltimateAuth")
+ public ClaimsPrincipal ToClaimsPrincipal(string authenticationType = UAuthConstants.SchemeDefaults.GlobalScheme)
{
if (!IsAuthenticated || Identity is null)
return new ClaimsPrincipal(new ClaimsIdentity());
diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateChangeReason.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateChangeReason.cs
similarity index 77%
rename from src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateChangeReason.cs
rename to src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateChangeReason.cs
index 587115bf..881df3f2 100644
--- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateChangeReason.cs
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateChangeReason.cs
@@ -5,5 +5,7 @@ public enum UAuthStateChangeReason
Authenticated,
Validated,
MarkedStale,
- Cleared
+ Cleared,
+ Touched,
+ Patched
}
diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEvent.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEvent.cs
new file mode 100644
index 00000000..6367d6a1
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEvent.cs
@@ -0,0 +1,15 @@
+namespace CodeBeam.UltimateAuth.Client;
+
+public enum UAuthStateEvent
+{
+ ValidationCalled,
+ IdentifiersChanged,
+ UserStatusChanged,
+ ProfileChanged,
+ CredentialsChanged,
+ CredentialsChangedSelf,
+ AuthorizationChanged,
+ SessionRevoked,
+ UserDeleted,
+ LogoutVariant
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventArgs.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventArgs.cs
new file mode 100644
index 00000000..7ec39119
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventArgs.cs
@@ -0,0 +1,16 @@
+namespace CodeBeam.UltimateAuth.Client;
+
+public abstract record UAuthStateEventArgs(
+ UAuthStateEvent Type,
+ UAuthStateEventHandlingMode RefreshMode);
+
+public sealed record UAuthStateEventArgs(
+ UAuthStateEvent Type,
+ UAuthStateEventHandlingMode RefreshMode,
+ TPayload Payload)
+ : UAuthStateEventArgs(Type, RefreshMode);
+
+public sealed record UAuthStateEventArgsEmpty(
+ UAuthStateEvent Type,
+ UAuthStateEventHandlingMode RefreshMode)
+ : UAuthStateEventArgs(Type, RefreshMode);
diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventHandlingMode.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventHandlingMode.cs
new file mode 100644
index 00000000..173f6526
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateEventHandlingMode.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Client;
+
+public enum UAuthStateEventHandlingMode
+{
+ Patch,
+ Validate,
+ None
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs
new file mode 100644
index 00000000..26399d5d
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthStateManager.cs
@@ -0,0 +1,145 @@
+using CodeBeam.UltimateAuth.Client.Events;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Users.Contracts;
+
+namespace CodeBeam.UltimateAuth.Client.Authentication;
+
+internal sealed class UAuthStateManager : IUAuthStateManager, IDisposable
+{
+ private Task? _ensureTask;
+ private readonly object _ensureLock = new();
+
+ private readonly IUAuthClient _client;
+ private readonly IUAuthClientEvents _events;
+ private readonly IClock _clock;
+
+ public UAuthState State { get; } = UAuthState.Anonymous();
+
+ public UAuthStateManager(IUAuthClient client, IUAuthClientEvents events, IClock clock)
+ {
+ _client = client;
+ _events = events;
+ _clock = clock;
+
+ _events.StateChanged += HandleStateEvent;
+ }
+
+ public Task EnsureAsync(bool force = false, CancellationToken ct = default)
+ {
+ if (!force && State.IsAuthenticated && !State.IsStale)
+ return Task.CompletedTask;
+
+ lock (_ensureLock)
+ {
+ if (_ensureTask != null)
+ return _ensureTask;
+
+ _ensureTask = EnsureInternalAsync(ct);
+ return _ensureTask;
+ }
+ }
+
+ private async Task EnsureInternalAsync(CancellationToken ct)
+ {
+ try
+ {
+ var result = await _client.Flows.ValidateAsync();
+
+ if (!result.IsValid || result.Snapshot == null)
+ {
+ if (State.IsAuthenticated)
+ State.MarkStale();
+ else
+ State.Clear();
+
+ return;
+ }
+
+ State.ApplySnapshot(result.Snapshot, _clock.UtcNow);
+ }
+ finally
+ {
+ lock (_ensureLock)
+ {
+ _ensureTask = null;
+ }
+ }
+ }
+
+ private async Task HandleStateEvent(UAuthStateEventArgs args)
+ {
+ if (args.RefreshMode == UAuthStateEventHandlingMode.None)
+ {
+ return;
+ }
+
+ switch (args.Type)
+ {
+ case UAuthStateEvent.SessionRevoked:
+ case UAuthStateEvent.CredentialsChanged:
+ case UAuthStateEvent.UserDeleted:
+ case UAuthStateEvent.LogoutVariant:
+ State.Clear();
+ return;
+ }
+
+ switch (args)
+ {
+ case UAuthStateEventArgs profile:
+ State.UpdateProfile(profile.Payload);
+ return;
+
+ case UAuthStateEventArgs profile:
+ State.UpdateUserStatus(profile.Payload);
+ return;
+ }
+
+ switch (args.RefreshMode)
+ {
+ case UAuthStateEventHandlingMode.Validate:
+ await EnsureAsync(true);
+ return;
+
+ case UAuthStateEventHandlingMode.Patch:
+ if (args.Type == UAuthStateEvent.ValidationCalled)
+ {
+ State.MarkValidated(_clock.UtcNow);
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ State.Touch(true);
+ }
+
+ public Task OnLoginAsync()
+ {
+ State.MarkStale();
+ return Task.CompletedTask;
+ }
+
+ public Task OnLogoutAsync()
+ {
+ State.Clear();
+ return Task.CompletedTask;
+ }
+
+ public void MarkStale()
+ {
+ State.MarkStale();
+ }
+
+ public void Clear()
+ {
+ State.Clear();
+ }
+
+ public void Dispose()
+ {
+ _events.StateChanged -= HandleStateEvent;
+ }
+
+ public bool NeedsValidation => !State.IsAuthenticated || State.IsStale;
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs
deleted file mode 100644
index 0751651d..00000000
--- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
-
-namespace CodeBeam.UltimateAuth.Client.Authentication;
-
-internal sealed class UAuthStateManager : IUAuthStateManager
-{
- private readonly IUAuthClient _client;
- private readonly IClock _clock;
-
- public UAuthState State { get; } = UAuthState.Anonymous();
-
- public UAuthStateManager(IUAuthClient client, IClock clock)
- {
- _client = client;
- _clock = clock;
- }
-
- public async Task EnsureAsync(bool force = false, CancellationToken ct = default)
- {
- if (!force && State.IsAuthenticated && !State.IsStale)
- return;
-
- var result = await _client.Flows.ValidateAsync();
-
- if (!result.IsValid || result.Snapshot == null)
- {
- if (State.IsAuthenticated)
- {
- State.MarkStale();
- return;
- }
-
- State.Clear();
- return;
- }
-
- State.ApplySnapshot(result.Snapshot, _clock.UtcNow);
- }
-
- public Task OnLoginAsync()
- {
- State.MarkStale();
- return Task.CompletedTask;
- }
-
- public Task OnLogoutAsync()
- {
- State.Clear();
- return Task.CompletedTask;
- }
-
- public void MarkStale()
- {
- State.MarkStale();
- }
-
- public bool NeedsValidation => !State.IsAuthenticated || State.IsStale;
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor b/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor
index f2b7a79d..d4942ba9 100644
--- a/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor
+++ b/src/CodeBeam.UltimateAuth.Client/Components/UALoginDispatch.razor
@@ -1,7 +1,7 @@
@page "/__uauth/login-redirect"
@namespace CodeBeam.UltimateAuth.Client
-@using CodeBeam.UltimateAuth.Core.Constants
+@using CodeBeam.UltimateAuth.Core.Defaults
@using Microsoft.AspNetCore.WebUtilities
@inject NavigationManager Nav
diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs
index 2c8c0786..cbddf34c 100644
--- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthApp.razor.cs
@@ -23,39 +23,37 @@ protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
- if (!firstRender)
- return;
+ if (firstRender)
+ {
+ if (_initialized)
+ return;
- if (_initialized)
- return;
+ _initialized = true;
- _initialized = true;
+ StateManager.State.RequestRender = () => InvokeAsync(StateHasChanged);
- await Bootstrapper.EnsureStartedAsync();
- await StateManager.EnsureAsync();
+ await Bootstrapper.EnsureStartedAsync();
+ await StateManager.EnsureAsync();
- if (StateManager.State.IsAuthenticated)
- {
- await Coordinator.StartAsync();
- _coordinatorStarted = true;
- }
+ if (StateManager.State.IsAuthenticated)
+ {
+ await Coordinator.StartAsync();
+ _coordinatorStarted = true;
+ }
- StateManager.State.Changed += OnStateChanged;
+ StateManager.State.Changed += OnStateChanged;
- StateHasChanged();
- }
+ StateHasChanged();
+ }
- private void OnStateChanged(UAuthStateChangeReason reason)
- {
- if (reason == UAuthStateChangeReason.MarkedStale)
+ if (StateManager.State.NeedsValidation)
{
- // Causes infinite loop
- //_ = InvokeAsync(async () =>
- //{
- // await StateManager.EnsureAsync();
- //});
+ await StateManager.EnsureAsync(true);
}
+ }
+ private void OnStateChanged(UAuthStateChangeReason reason)
+ {
if (reason == UAuthStateChangeReason.Authenticated)
{
_ = InvokeAsync(async () =>
diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs
index 34a3ffc6..f4723276 100644
--- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthFlowPageBase.cs
@@ -13,6 +13,7 @@ public abstract class UAuthFlowPageBase : UAuthReactiveComponentBase
protected AuthFlowPayload? UAuthPayload { get; private set; }
protected string? ReturnUrl { get; private set; }
protected bool ShouldFocus { get; private set; }
+ protected string? Identifier { get; private set; }
protected virtual bool ClearQueryAfterParse => true;
@@ -39,6 +40,7 @@ protected override void OnParametersSet()
ShouldFocus = query.TryGetValue("focus", out var focus) && focus == "1";
ReturnUrl = query.TryGetValue("returnUrl", out var ru) ? ru.ToString() : null;
+ Identifier = query.TryGetValue("identifier", out var id) ? id.ToString() : null;
UAuthPayload = null;
diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor
index 75273832..d8d4dddf 100644
--- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor
+++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor
@@ -4,7 +4,7 @@
@using CodeBeam.UltimateAuth.Client.Device
@using CodeBeam.UltimateAuth.Client.Options
@using CodeBeam.UltimateAuth.Core.Abstractions
-@using CodeBeam.UltimateAuth.Core.Constants
+@using CodeBeam.UltimateAuth.Core.Defaults
@using CodeBeam.UltimateAuth.Core.Contracts
@using CodeBeam.UltimateAuth.Core.Options
@using Microsoft.Extensions.Options
diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs
index a820233d..cd7de6dc 100644
--- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthLoginForm.razor.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Constants;
using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor
index 95894528..bbcbab52 100644
--- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor
+++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor
@@ -2,7 +2,9 @@
@inherits UAuthReactiveComponentBase
@using CodeBeam.UltimateAuth.Core.Domain
+@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
+@inject IAuthorizationService AuthorizationService
@if (_inactive)
{
@@ -15,21 +17,22 @@
@NotAuthorized
}
}
+else if (_authorizing && Authorizing is not null)
+{
+ @Authorizing
+}
+else if (!_authorized)
+{
+ @NotAuthorized
+}
else
{
-
-
- @if (Authorized is not null)
- {
- @Authorized(AuthState)
- }
- else if (ChildContent is not null)
- {
- @ChildContent(AuthState)
- }
-
-
- @NotAuthorized
-
-
+ if (Authorized is not null)
+ {
+ @Authorized(AuthState)
+ }
+ else if (ChildContent is not null)
+ {
+ @ChildContent(AuthState)
+ }
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs
index 824f7107..23dc02e9 100644
--- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthStateView.razor.cs
@@ -1,10 +1,20 @@
using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
namespace CodeBeam.UltimateAuth.Client;
public partial class UAuthStateView : UAuthReactiveComponentBase
{
+ private IReadOnlyList _rolesParsed = Array.Empty();
+ private IReadOnlyList _permissionsParsed = Array.Empty();
+ private bool _authorized;
+ private bool _inactive;
+ private bool _authorizing;
+ private string? _authKey;
+ private string? _rolesRaw;
+ private string? _permissionsRaw;
+
[Parameter]
public RenderFragment? Authorized { get; set; }
@@ -14,29 +24,94 @@ public partial class UAuthStateView : UAuthReactiveComponentBase
[Parameter]
public RenderFragment? Inactive { get; set; }
+ [Parameter]
+ public RenderFragment? Authorizing { get; set; }
+
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string? Roles { get; set; }
+ [Parameter]
+ public string? Permissions { get; set; }
+
[Parameter]
public string? Policy { get; set; }
+ ///
+ /// Gets or sets a value indicating whether all set conditions must be matched for the operation to succeed.
+ /// Null parameters don't count as condition.
+ ///
[Parameter]
- public bool RequireActive { get; set; } = true;
+ public bool MatchAll { get; set; } = true;
- private bool _inactive;
+ [Parameter]
+ public bool RequireActive { get; set; } = true;
- protected override void OnParametersSet()
+ protected override async Task OnParametersSetAsync()
{
- base.OnParametersSet();
+ await base.OnParametersSetAsync();
+
+ var newKey = BuildAuthKey();
+
+ if (_authKey == newKey)
+ return;
+
+ _authKey = newKey;
+ _authorizing = true;
+
+ if (_rolesRaw != Roles)
+ {
+ _rolesRaw = Roles;
+ _rolesParsed = ParseCsv(Roles);
+ }
+
+ if (_permissionsRaw != Permissions)
+ {
+ _permissionsRaw = Permissions;
+ _permissionsParsed = ParseCsv(Permissions);
+ }
+
EvaluateSessionState();
+ _authorized = await EvaluateAuthorizationAsync();
+ _authorizing = false;
}
- protected override void HandleAuthStateChanged(UAuthStateChangeReason reason)
+ protected override async void HandleAuthStateChanged(UAuthStateChangeReason reason)
{
EvaluateSessionState();
+ _authorizing = true;
+ _authorized = await EvaluateAuthorizationAsync();
+ _authorizing = false;
+ await InvokeAsync(StateHasChanged);
+ }
+
+ private async Task EvaluateAuthorizationAsync()
+ {
+ if (!AuthState.IsAuthenticated)
+ return false;
+
+ var roles = _rolesParsed;
+ var permissions = _permissionsParsed;
+
+ var results = new List();
+
+ if (roles.Count > 0)
+ results.Add(roles.Any(AuthState.IsInRole));
+
+ if (permissions.Count > 0)
+ results.Add(permissions.Any(AuthState.HasPermission));
+
+ if (!string.IsNullOrWhiteSpace(Policy))
+ results.Add(await EvaluatePolicyAsync());
+
+ if (results.Count == 0)
+ return true;
+
+ return MatchAll
+ ? results.All(x => x)
+ : results.Any(x => x);
}
private void EvaluateSessionState()
@@ -62,4 +137,32 @@ private void EvaluateSessionState()
_inactive = AuthState.Identity?.SessionState != SessionState.Active;
}
}
+
+ private static IReadOnlyList ParseCsv(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return Array.Empty();
+
+ return value
+ .Split(',', StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Trim())
+ .Where(x => x.Length > 0)
+ .ToArray();
+ }
+
+ private async Task EvaluatePolicyAsync()
+ {
+ if (string.IsNullOrWhiteSpace(Policy))
+ return true;
+
+ var principal = AuthState.ToClaimsPrincipal();
+ var result = await AuthorizationService.AuthorizeAsync(principal, Policy);
+
+ return result.Succeeded;
+ }
+
+ private string BuildAuthKey()
+ {
+ return $"{Roles}|{Permissions}|{Policy}|{MatchAll}";
+ }
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs b/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs
index 2c529c99..e2173571 100644
--- a/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Device/BrowserDeviceIdStorage.cs
@@ -15,10 +15,17 @@ public BrowserDeviceIdStorage(IBrowserStorage storage)
public async ValueTask LoadAsync(CancellationToken ct = default)
{
- if (!await _storage.ExistsAsync(StorageScope.Local, Key))
- return null;
+ try
+ {
+ if (!await _storage.ExistsAsync(StorageScope.Local, Key))
+ return null;
- return await _storage.GetAsync(StorageScope.Local, Key);
+ return await _storage.GetAsync(StorageScope.Local, Key);
+ }
+ catch (TaskCanceledException)
+ {
+ return null;
+ }
}
public ValueTask SaveAsync(string deviceId, CancellationToken ct = default)
diff --git a/src/CodeBeam.UltimateAuth.Client/Events/IUAuthClientEvents.cs b/src/CodeBeam.UltimateAuth.Client/Events/IUAuthClientEvents.cs
new file mode 100644
index 00000000..dfe52ad2
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Events/IUAuthClientEvents.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Client.Events;
+
+public interface IUAuthClientEvents
+{
+ event Func? StateChanged;
+ Task PublishAsync(UAuthStateEventArgs args);
+ Task PublishAsync(UAuthStateEventArgs args);
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Events/UAuthClientEvents.cs b/src/CodeBeam.UltimateAuth.Client/Events/UAuthClientEvents.cs
new file mode 100644
index 00000000..395126c3
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Events/UAuthClientEvents.cs
@@ -0,0 +1,21 @@
+namespace CodeBeam.UltimateAuth.Client.Events;
+
+internal sealed class UAuthClientEvents : IUAuthClientEvents
+{
+ public event Func? StateChanged;
+
+ public Task PublishAsync(UAuthStateEventArgs args)
+ => PublishAsync((UAuthStateEventArgs)args);
+
+ public async Task PublishAsync(UAuthStateEventArgs args)
+ {
+ var handlers = StateChanged;
+ if (handlers == null)
+ return;
+
+ foreach (var handler in handlers.GetInvocationList())
+ {
+ await ((Func)handler)(args);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs
index 5c4e1525..9223b29b 100644
--- a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs
@@ -3,6 +3,7 @@
using CodeBeam.UltimateAuth.Client.Device;
using CodeBeam.UltimateAuth.Client.Devices;
using CodeBeam.UltimateAuth.Client.Diagnostics;
+using CodeBeam.UltimateAuth.Client.Events;
using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Client.Runtime;
@@ -66,6 +67,7 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol
services.AddSingleton();
services.AddSingleton, UAuthClientOptionsPostConfigure>();
services.TryAddSingleton();
+ services.AddSingleton();
services.PostConfigure(o =>
{
@@ -75,6 +77,7 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageDiscovery.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageDiscovery.cs
index 171bbabd..7e1d5999 100644
--- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageDiscovery.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/Login/UAuthLoginPageDiscovery.cs
@@ -26,7 +26,7 @@ public static string Resolve()
return _cached = "/login";
if (candidates.Count > 1)
- throw new InvalidOperationException("Multiple [UAuthLoginPage] found. Define Navigation.LoginResolver explicitly.");
+ throw new InvalidOperationException("Multiple [UAuthLoginPage] found. Make sure you only have one login page that attribute defined or define Navigation.LoginResolver explicitly.");
var routeAttr = candidates[0].GetCustomAttributes(typeof(RouteAttribute), true).FirstOrDefault() as RouteAttribute;
diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs
index 96a79bec..a6de47b9 100644
--- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/SessionCoordinator.cs
@@ -2,30 +2,33 @@
using CodeBeam.UltimateAuth.Client.Contracts;
using CodeBeam.UltimateAuth.Client.Diagnostics;
using CodeBeam.UltimateAuth.Client.Options;
+using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
namespace CodeBeam.UltimateAuth.Client.Infrastructure;
+// TODO: Add multi tab single refresh support
internal sealed class SessionCoordinator : ISessionCoordinator
{
private readonly IUAuthClient _client;
private readonly NavigationManager _navigation;
private readonly UAuthClientOptions _options;
private readonly UAuthClientDiagnostics _diagnostics;
+ private readonly IClock _clock;
- private PeriodicTimer? _timer;
private CancellationTokenSource? _cts;
public event Action? ReauthRequired;
- public SessionCoordinator(IUAuthClient client, NavigationManager navigation, IOptions options, UAuthClientDiagnostics diagnostics)
+ public SessionCoordinator(IUAuthClient client, NavigationManager navigation, IOptions options, UAuthClientDiagnostics diagnostics, IClock clock)
{
_client = client;
_navigation = navigation;
_options = options.Value;
_diagnostics = diagnostics;
+ _clock = clock;
}
public async Task StartAsync(CancellationToken cancellationToken = default)
@@ -33,59 +36,32 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
if (!_options.AutoRefresh.Enabled)
return;
- if (_timer is not null)
+ if (_cts is not null)
return;
_diagnostics.MarkStarted();
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- var interval = _options.AutoRefresh.Interval ?? TimeSpan.FromMinutes(5);
- _timer = new PeriodicTimer(interval);
_ = RunAsync(_cts.Token);
}
private async Task RunAsync(CancellationToken ct)
{
+ var interval = _options.AutoRefresh.Interval ?? TimeSpan.FromMinutes(5);
+
try
{
- while (await _timer!.WaitForNextTickAsync(ct))
+ while (!ct.IsCancellationRequested)
{
- _diagnostics.MarkAutomaticRefresh();
- var result = await _client.Flows.RefreshAsync(isAuto: true);
-
- switch (result.Outcome)
- {
- case RefreshOutcome.Touched:
- break;
-
- case RefreshOutcome.NoOp:
- break;
-
- case RefreshOutcome.Success:
- break;
-
- case RefreshOutcome.ReauthRequired:
- switch (_options.Reauth.Behavior)
- {
- case ReauthBehavior.Redirect:
- _navigation.NavigateTo(_options.Reauth.RedirectPath ?? _options.Endpoints.Login, forceLoad: true);
- break;
-
- case ReauthBehavior.RaiseEvent:
- ReauthRequired?.Invoke();
- break;
-
- case ReauthBehavior.None:
- break;
- }
- _diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired);
- return;
- }
+ await Task.Delay(interval, ct);
+ await TickAsync();
+
+ if (_diagnostics.IsTerminated)
+ return;
}
}
catch (OperationCanceledException)
{
- // expected
}
}
@@ -93,8 +69,6 @@ public Task StopAsync()
{
_diagnostics.MarkStopped();
_cts?.Cancel();
- _timer?.Dispose();
- _timer = null;
return Task.CompletedTask;
}
@@ -102,4 +76,29 @@ public async ValueTask DisposeAsync()
{
await StopAsync();
}
+
+ internal async Task TickAsync()
+ {
+ _diagnostics.MarkAutomaticRefresh();
+
+ var result = await _client.Flows.RefreshAsync(true);
+
+ if (result.Outcome != RefreshOutcome.ReauthRequired)
+ return;
+
+ if (result.Outcome == RefreshOutcome.ReauthRequired)
+ {
+ switch (_options.Reauth.Behavior)
+ {
+ case ReauthBehavior.Redirect: _navigation.NavigateTo(_options.Reauth.RedirectPath ?? _options.Endpoints.Login, forceLoad: true);
+ break;
+
+ case ReauthBehavior.RaiseEvent:
+ ReauthRequired?.Invoke();
+ break;
+ }
+
+ _diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired);
+ }
+ }
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
index dab4589d..7931121d 100644
--- a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
@@ -13,6 +13,7 @@ public sealed class UAuthClientOptions
///
public string? DefaultReturnUrl { get; set; }
+ public UAuthStateEventOptions StateEvents { get; set; } = new();
public UAuthClientEndpointOptions Endpoints { get; set; } = new();
public UAuthClientLoginFlowOptions Login { get; set; } = new();
diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthStateEventOptions.cs b/src/CodeBeam.UltimateAuth.Client/Options/UAuthStateEventOptions.cs
new file mode 100644
index 00000000..51bf9632
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Options/UAuthStateEventOptions.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Client.Options;
+
+public class UAuthStateEventOptions
+{
+ public UAuthStateEventHandlingMode HandlingMode { get; set; } = UAuthStateEventHandlingMode.Patch;
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs
new file mode 100644
index 00000000..6e45c78c
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs
@@ -0,0 +1,21 @@
+using CodeBeam.UltimateAuth.Authorization;
+using CodeBeam.UltimateAuth.Authorization.Contracts;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Client.Services;
+
+public interface IAuthorizationClient
+{
+ Task> CheckAsync(AuthorizationCheckRequest request);
+ Task> GetMyRolesAsync(PageRequest? request = null);
+ Task> GetUserRolesAsync(UserKey userKey, PageRequest? request = null);
+ Task AssignRoleToUserAsync(UserKey userKey, string role);
+ Task RemoveRoleFromUserAsync(UserKey userKey, string role);
+
+ Task> CreateRoleAsync(CreateRoleRequest request);
+ Task>> QueryRolesAsync(RoleQuery request);
+ Task RenameRoleAsync(RoleId roleId, RenameRoleRequest request);
+ Task SetPermissionsAsync(RoleId roleId, SetPermissionsRequest request);
+ Task> DeleteRoleAsync(RoleId roleId, DeleteRoleRequest request);
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs
new file mode 100644
index 00000000..1bd06422
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs
@@ -0,0 +1,21 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Credentials.Contracts;
+
+namespace CodeBeam.UltimateAuth.Client.Services;
+
+public interface ICredentialClient
+{
+ Task> AddMyAsync(AddCredentialRequest request);
+ Task> ChangeMyAsync(ChangeCredentialRequest request);
+ Task RevokeMyAsync(RevokeCredentialRequest request);
+ Task> BeginResetMyAsync(BeginCredentialResetRequest request);
+ Task> CompleteResetMyAsync(CompleteCredentialResetRequest request);
+
+ Task> AddCredentialAsync(UserKey userKey, AddCredentialRequest request);
+ Task> ChangeCredentialAsync(UserKey userKey, ChangeCredentialRequest request);
+ Task RevokeCredentialAsync(UserKey userKey, RevokeCredentialRequest request);
+ Task> BeginResetCredentialAsync(UserKey userKey, BeginCredentialResetRequest request);
+ Task> CompleteResetCredentialAsync(UserKey userKey, CompleteCredentialResetRequest request);
+ Task DeleteCredentialAsync(UserKey userKey);
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs
new file mode 100644
index 00000000..fade45c2
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs
@@ -0,0 +1,26 @@
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Users.Contracts;
+
+// TODO: Add ReauthAsync
+namespace CodeBeam.UltimateAuth.Client.Services;
+
+public interface IFlowClient
+{
+ Task LoginAsync(LoginRequest request, string? returnUrl = null);
+ Task LogoutAsync();
+ Task RefreshAsync(bool isAuto = false);
+ //Task ReauthAsync();
+ Task ValidateAsync();
+
+ Task BeginPkceAsync(string? returnUrl = null);
+ Task CompletePkceLoginAsync(PkceLoginRequest request);
+
+ Task> LogoutDeviceSelfAsync(LogoutDeviceRequest request);
+ Task LogoutOtherDevicesSelfAsync();
+ Task LogoutAllDevicesSelfAsync();
+ Task> LogoutDeviceAdminAsync(UserKey userKey, LogoutDeviceRequest request);
+ Task LogoutOtherDevicesAdminAsync(UserKey userKey, LogoutOtherDevicesAdminRequest request);
+ Task LogoutAllDevicesAdminAsync(UserKey userKey);
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs
new file mode 100644
index 00000000..d4812565
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs
@@ -0,0 +1,21 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Client.Services;
+
+public interface ISessionClient
+{
+ Task>> GetMyChainsAsync(PageRequest? request = null);
+ Task> GetMyChainDetailAsync(SessionChainId chainId);
+ Task> RevokeMyChainAsync(SessionChainId chainId);
+ Task RevokeMyOtherChainsAsync();
+ Task RevokeAllMyChainsAsync();
+
+
+ Task>> GetUserChainsAsync(UserKey userKey, PageRequest? request = null);
+ Task> GetUserChainDetailAsync(UserKey userKey, SessionChainId chainId);
+ Task RevokeUserSessionAsync(UserKey userKey, AuthSessionId sessionId);
+ Task> RevokeUserChainAsync(UserKey userKey, SessionChainId chainId);
+ Task RevokeUserRootAsync(UserKey userKey);
+ Task RevokeAllUserChainsAsync(UserKey userKey);
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs
similarity index 89%
rename from src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs
rename to src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs
index 272959ef..54dcf6bd 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs
@@ -5,6 +5,7 @@ namespace CodeBeam.UltimateAuth.Client;
public interface IUAuthClient
{
IFlowClient Flows { get; }
+ ISessionClient Sessions { get; }
IUserClient Users { get; }
IUserIdentifierClient Identifiers { get; }
ICredentialClient Credentials { get; }
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
similarity index 56%
rename from src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs
rename to src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
index 88914fff..c4f10ce0 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
@@ -6,14 +6,17 @@ namespace CodeBeam.UltimateAuth.Client.Services;
public interface IUserClient
{
+ Task>> QueryUsersAsync(UserQuery query);
Task> CreateAsync(CreateUserRequest request);
+ Task> CreateAdminAsync(CreateUserRequest request);
Task> ChangeStatusSelfAsync(ChangeUserStatusSelfRequest request);
- Task> ChangeStatusAdminAsync(ChangeUserStatusAdminRequest request);
- Task> DeleteAsync(DeleteUserRequest request);
+ Task> ChangeStatusAdminAsync(UserKey userKey, ChangeUserStatusAdminRequest request);
+ Task DeleteMeAsync();
+ Task> DeleteUserAsync(UserKey userKey, DeleteUserRequest request);
- Task> GetMeAsync();
+ Task> GetMeAsync();
Task UpdateMeAsync(UpdateProfileRequest request);
- Task> GetProfileAsync(UserKey userKey);
+ Task> GetProfileAsync(UserKey userKey);
Task UpdateProfileAsync(UserKey userKey, UpdateProfileRequest request);
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUserIdentifierClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
similarity index 84%
rename from src/CodeBeam.UltimateAuth.Client/Services/IUserIdentifierClient.cs
rename to src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
index 7f019423..cc2190e8 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/IUserIdentifierClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
@@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Client.Services;
public interface IUserIdentifierClient
{
- Task>> GetMyIdentifiersAsync();
+ Task>> GetMyIdentifiersAsync(PageRequest? request = null);
Task AddSelfAsync(AddUserIdentifierRequest request);
Task UpdateSelfAsync(UpdateUserIdentifierRequest request);
Task SetPrimarySelfAsync(SetPrimaryUserIdentifierRequest request);
@@ -14,7 +14,7 @@ public interface IUserIdentifierClient
Task VerifySelfAsync(VerifyUserIdentifierRequest request);
Task DeleteSelfAsync(DeleteUserIdentifierRequest request);
- Task>> GetUserIdentifiersAsync(UserKey userKey);
+ Task>> GetUserIdentifiersAsync(UserKey userKey, PageRequest? request = null);
Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request);
Task UpdateAdminAsync(UserKey userKey, UpdateUserIdentifierRequest request);
Task SetPrimaryAdminAsync(UserKey userKey, SetPrimaryUserIdentifierRequest request);
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IAuthorizationClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IAuthorizationClient.cs
deleted file mode 100644
index 192ef5c0..00000000
--- a/src/CodeBeam.UltimateAuth.Client/Services/IAuthorizationClient.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using CodeBeam.UltimateAuth.Authorization.Contracts;
-using CodeBeam.UltimateAuth.Core.Contracts;
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Client.Services;
-
-public interface IAuthorizationClient
-{
- Task> CheckAsync(AuthorizationCheckRequest request);
-
- Task> GetMyRolesAsync();
-
- Task> GetUserRolesAsync(UserKey userKey);
-
- Task AssignRoleAsync(UserKey userKey, string role);
-
- Task RemoveRoleAsync(UserKey userKey, string role);
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/ICredentialClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/ICredentialClient.cs
deleted file mode 100644
index eb92db92..00000000
--- a/src/CodeBeam.UltimateAuth.Client/Services/ICredentialClient.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Contracts;
-using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Credentials.Contracts;
-
-namespace CodeBeam.UltimateAuth.Client.Services;
-
-public interface ICredentialClient
-{
- Task> GetMyAsync();
- Task> AddMyAsync(AddCredentialRequest request);
- Task> ChangeMyAsync(CredentialType type, ChangeCredentialRequest request);
- Task RevokeMyAsync(CredentialType type, RevokeCredentialRequest request);
- Task BeginResetMyAsync(CredentialType type, BeginCredentialResetRequest request);
- Task CompleteResetMyAsync(CredentialType type, CompleteCredentialResetRequest request);
-
- Task> GetUserAsync(UserKey userKey);
- Task> AddUserAsync(UserKey userKey, AddCredentialRequest request);
- Task RevokeUserAsync(UserKey userKey, CredentialType type, RevokeCredentialRequest request);
- Task ActivateUserAsync(UserKey userKey, CredentialType type);
- Task BeginResetUserAsync(UserKey userKey, CredentialType type, BeginCredentialResetRequest request);
- Task CompleteResetUserAsync(UserKey userKey, CredentialType type, CompleteCredentialResetRequest request);
- Task DeleteUserAsync(UserKey userKey, CredentialType type);
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IFlowClient.cs
deleted file mode 100644
index bec146de..00000000
--- a/src/CodeBeam.UltimateAuth.Client/Services/IFlowClient.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using CodeBeam.UltimateAuth.Client.Contracts;
-using CodeBeam.UltimateAuth.Core.Contracts;
-
-// TODO: Add ReauthAsync
-namespace CodeBeam.UltimateAuth.Client.Services;
-
-public interface IFlowClient
-{
- Task LoginAsync(LoginRequest request, string? returnUrl = null);
- Task LogoutAsync();
- Task RefreshAsync(bool isAuto = false);
- //Task ReauthAsync();
- Task ValidateAsync();
-
- Task BeginPkceAsync(string? returnUrl = null);
- Task CompletePkceLoginAsync(PkceLoginRequest request);
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs
index 4cfc6689..683db2bc 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs
@@ -1,4 +1,6 @@
-using CodeBeam.UltimateAuth.Authorization.Contracts;
+using CodeBeam.UltimateAuth.Authorization;
+using CodeBeam.UltimateAuth.Authorization.Contracts;
+using CodeBeam.UltimateAuth.Client.Events;
using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Core.Contracts;
@@ -10,11 +12,13 @@ namespace CodeBeam.UltimateAuth.Client.Services;
internal sealed class UAuthAuthorizationClient : IAuthorizationClient
{
private readonly IUAuthRequestClient _request;
+ private readonly IUAuthClientEvents _events;
private readonly UAuthClientOptions _options;
- public UAuthAuthorizationClient(IUAuthRequestClient request, IOptions options)
+ public UAuthAuthorizationClient(IUAuthRequestClient request, IUAuthClientEvents events, IOptions options)
{
_request = request;
+ _events = events;
_options = options.Value;
}
@@ -26,35 +30,102 @@ public async Task> CheckAsync(AuthorizationChec
return UAuthResultMapper.FromJson(raw);
}
- public async Task> GetMyRolesAsync()
+ public async Task> GetMyRolesAsync(PageRequest? request = null)
{
- var raw = await _request.SendFormAsync(Url("/authorization/users/me/roles/get"));
+ request ??= new PageRequest();
+ var raw = await _request.SendJsonAsync(Url("/me/authorization/roles/get"), request);
return UAuthResultMapper.FromJson(raw);
}
- public async Task> GetUserRolesAsync(UserKey userKey)
+ public async Task> GetUserRolesAsync(UserKey userKey, PageRequest? request = null)
{
- var raw = await _request.SendFormAsync(Url($"/admin/authorization/users/{userKey}/roles/get"));
+ request ??= new PageRequest();
+ var raw = await _request.SendJsonAsync(Url($"/admin/authorization/users/{userKey}/roles/get"), request);
return UAuthResultMapper.FromJson(raw);
}
- public async Task AssignRoleAsync(UserKey userKey, string role)
+ public async Task AssignRoleToUserAsync(UserKey userKey, string role)
{
- var raw = await _request.SendJsonAsync(Url($"/admin/authorization/users/{userKey}/roles/post"), new AssignRoleRequest
+ var raw = await _request.SendJsonAsync(Url($"/admin/authorization/users/{userKey}/roles/assign"), new AssignRoleRequest
{
Role = role
});
- return UAuthResultMapper.From(raw);
+ var result = UAuthResultMapper.From(raw);
+
+ if (result.IsSuccess)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.AuthorizationChanged, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
}
- public async Task RemoveRoleAsync(UserKey userKey, string role)
+ public async Task RemoveRoleFromUserAsync(UserKey userKey, string role)
{
- var raw = await _request.SendJsonAsync(Url($"/admin/authorization/users/{userKey}/roles/delete"), new AssignRoleRequest
+ var raw = await _request.SendJsonAsync(Url($"/admin/authorization/users/{userKey}/roles/remove"), new AssignRoleRequest
{
Role = role
});
- return UAuthResultMapper.From(raw);
+ var result = UAuthResultMapper.From(raw);
+
+ if (result.IsSuccess)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.AuthorizationChanged, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
+ }
+
+ public async Task> CreateRoleAsync(CreateRoleRequest request)
+ {
+ var raw = await _request.SendJsonAsync(Url("/admin/authorization/roles/create"), request);
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task>> QueryRolesAsync(RoleQuery request)
+ {
+ var raw = await _request.SendJsonAsync(Url("/admin/authorization/roles/query"), request);
+ return UAuthResultMapper.FromJson>(raw);
+ }
+
+ public async Task RenameRoleAsync(RoleId roleId, RenameRoleRequest request)
+ {
+ var raw = await _request.SendJsonAsync(Url($"/admin/authorization/roles/{roleId}/rename"), request);
+ var result = UAuthResultMapper.From(raw);
+
+ if (result.IsSuccess)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.AuthorizationChanged, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
+ }
+
+ public async Task SetPermissionsAsync(RoleId roleId, SetPermissionsRequest request)
+ {
+ var raw = await _request.SendJsonAsync(Url($"/admin/authorization/roles/{roleId}/permissions"), request);
+ var result = UAuthResultMapper.From(raw);
+
+ if (result.IsSuccess)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.AuthorizationChanged, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
+ }
+
+ public async Task> DeleteRoleAsync(RoleId roleId, DeleteRoleRequest request)
+ {
+ var raw = await _request.SendJsonAsync(Url($"/admin/authorization/roles/{roleId}/delete"), request);
+ var result = UAuthResultMapper.FromJson(raw);
+
+ if (result.IsSuccess)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.AuthorizationChanged, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
}
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs
index 40d44f2a..6df7e74d 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs
@@ -5,14 +5,16 @@ namespace CodeBeam.UltimateAuth.Client;
internal sealed class UAuthClient : IUAuthClient
{
public IFlowClient Flows { get; }
+ public ISessionClient Sessions { get; }
public IUserClient Users { get; }
public IUserIdentifierClient Identifiers { get; }
public ICredentialClient Credentials { get; }
public IAuthorizationClient Authorization { get; }
- public UAuthClient(IFlowClient flows, IUserClient users, IUserIdentifierClient identifiers, ICredentialClient credentials, IAuthorizationClient authorization)
+ public UAuthClient(IFlowClient flows, ISessionClient session, IUserClient users, IUserIdentifierClient identifiers, ICredentialClient credentials, IAuthorizationClient authorization)
{
Flows = flows;
+ Sessions = session;
Users = users;
Identifiers = identifiers;
Credentials = credentials;
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs
index 1ef35801..ca73037a 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs
@@ -1,4 +1,5 @@
-using CodeBeam.UltimateAuth.Client.Infrastructure;
+using CodeBeam.UltimateAuth.Client.Events;
+using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
@@ -10,93 +11,94 @@ namespace CodeBeam.UltimateAuth.Client.Services;
internal sealed class UAuthCredentialClient : ICredentialClient
{
private readonly IUAuthRequestClient _request;
+ private readonly IUAuthClientEvents _events;
private readonly UAuthClientOptions _options;
- public UAuthCredentialClient(IUAuthRequestClient request, IOptions options)
+ public UAuthCredentialClient(IUAuthRequestClient request, IUAuthClientEvents events, IOptions options)
{
_request = request;
+ _events = events;
_options = options.Value;
}
private string Url(string path) => UAuthUrlBuilder.Build(_options.Endpoints.BasePath, path, _options.MultiTenant);
- public async Task> GetMyAsync()
- {
- var raw = await _request.SendFormAsync(Url("/credentials/get"));
- return UAuthResultMapper.FromJson(raw);
- }
-
public async Task> AddMyAsync(AddCredentialRequest request)
{
- var raw = await _request.SendJsonAsync(Url("/credentials/add"), request);
+ var raw = await _request.SendJsonAsync(Url("/me/credentials/add"), request);
return UAuthResultMapper.FromJson(raw);
}
- public async Task> ChangeMyAsync(CredentialType type, ChangeCredentialRequest request)
+ public async Task> ChangeMyAsync(ChangeCredentialRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/change"), request);
+ var raw = await _request.SendJsonAsync(Url("/me/credentials/change"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.CredentialsChangedSelf, _options.StateEvents.HandlingMode));
+ }
return UAuthResultMapper.FromJson(raw);
}
- public async Task RevokeMyAsync(CredentialType type, RevokeCredentialRequest request)
+ public async Task RevokeMyAsync(RevokeCredentialRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/revoke"), request);
+ var raw = await _request.SendJsonAsync(Url($"/me/credentials/revoke"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.CredentialsChanged, _options.StateEvents.HandlingMode));
+ }
return UAuthResultMapper.From(raw);
}
- public async Task BeginResetMyAsync(CredentialType type, BeginCredentialResetRequest request)
+ public async Task> BeginResetMyAsync(BeginCredentialResetRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/reset/begin"), request);
- return UAuthResultMapper.From(raw);
+ var raw = await _request.SendJsonAsync(Url($"/me/credentials/reset/begin"), request);
+ return UAuthResultMapper.FromJson(raw);
}
- public async Task CompleteResetMyAsync(CredentialType type, CompleteCredentialResetRequest request)
+ public async Task> CompleteResetMyAsync(CompleteCredentialResetRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/reset/complete"), request);
- return UAuthResultMapper.From(raw);
+ var raw = await _request.SendJsonAsync(Url($"/me/credentials/reset/complete"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.CredentialsChanged, _options.StateEvents.HandlingMode));
+ }
+ return UAuthResultMapper.FromJson(raw);
}
- public async Task> GetUserAsync(UserKey userKey)
- {
- var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/get"));
- return UAuthResultMapper.FromJson(raw);
- }
-
- public async Task> AddUserAsync(UserKey userKey, AddCredentialRequest request)
+ public async Task> AddCredentialAsync(UserKey userKey, AddCredentialRequest request)
{
var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/add"), request);
return UAuthResultMapper.FromJson(raw);
}
- public async Task RevokeUserAsync(UserKey userKey, CredentialType type, RevokeCredentialRequest request)
+ public async Task> ChangeCredentialAsync(UserKey userKey, ChangeCredentialRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/revoke"), request);
- return UAuthResultMapper.From(raw);
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/change"), request);
+ return UAuthResultMapper.FromJson(raw);
}
- public async Task ActivateUserAsync(UserKey userKey, CredentialType type)
+ public async Task RevokeCredentialAsync(UserKey userKey, RevokeCredentialRequest request)
{
- var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/{type}/activate"));
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/revoke"), request);
return UAuthResultMapper.From(raw);
}
- public async Task BeginResetUserAsync(UserKey userKey, CredentialType type, BeginCredentialResetRequest request)
+ public async Task> BeginResetCredentialAsync(UserKey userKey, BeginCredentialResetRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/reset/begin"), request);
- return UAuthResultMapper.From(raw);
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/reset/begin"), request);
+ return UAuthResultMapper.FromJson(raw);
}
- public async Task CompleteResetUserAsync(UserKey userKey, CredentialType type, CompleteCredentialResetRequest request)
+ public async Task> CompleteResetCredentialAsync(UserKey userKey, CompleteCredentialResetRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/reset/complete"), request);
- return UAuthResultMapper.From(raw);
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/reset/complete"), request);
+ return UAuthResultMapper.FromJson(raw);
}
- public async Task DeleteUserAsync(UserKey userKey, CredentialType type)
+ public async Task DeleteCredentialAsync(UserKey userKey)
{
- var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/{type}/delete"));
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/delete"));
return UAuthResultMapper.From(raw);
}
-
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
index 3c81d96e..4a04c194 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
@@ -1,12 +1,14 @@
using CodeBeam.UltimateAuth.Client.Contracts;
using CodeBeam.UltimateAuth.Client.Diagnostics;
using CodeBeam.UltimateAuth.Client.Errors;
+using CodeBeam.UltimateAuth.Client.Events;
using CodeBeam.UltimateAuth.Client.Extensions;
using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Infrastructure;
+using CodeBeam.UltimateAuth.Users.Contracts;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using System.Net;
@@ -19,13 +21,15 @@ namespace CodeBeam.UltimateAuth.Client.Services;
internal class UAuthFlowClient : IFlowClient
{
private readonly IUAuthRequestClient _post;
+ private readonly IUAuthClientEvents _events;
private readonly UAuthClientOptions _options;
private readonly UAuthClientDiagnostics _diagnostics;
private readonly NavigationManager _nav;
- public UAuthFlowClient(IUAuthRequestClient post, IOptions options, UAuthClientDiagnostics diagnostics, NavigationManager nav)
+ public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IOptions options, UAuthClientDiagnostics diagnostics, NavigationManager nav)
{
_post = post;
+ _events = events;
_options = options.Value;
_diagnostics = diagnostics;
_nav = nav;
@@ -149,7 +153,11 @@ public async Task ValidateAsync()
throw new UAuthProtocolException("Malformed validation response.");
if (raw.Status == 401 || (raw.Status >= 200 && raw.Status < 300))
+ {
+ // Don't set refresh mode to validate here, it's already validated.
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.ValidationCalled, UAuthStateEventHandlingMode.Patch));
return body;
+ }
if (raw.Status >= 400 && raw.Status < 500)
throw new UAuthProtocolException($"Unexpected client error during validation: {raw.Status}");
@@ -227,6 +235,60 @@ public async Task CompletePkceLoginAsync(PkceLoginRequest request)
await _post.NavigateAsync(url, payload);
}
+ public async Task> LogoutDeviceSelfAsync(LogoutDeviceRequest request)
+ {
+ var raw = await _post.SendJsonAsync(Url($"/me/logout-device"), request);
+
+ if (raw.Ok)
+ {
+ var result = UAuthResultMapper.FromJson(raw);
+
+ if (result.Value?.CurrentChain == true)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.LogoutVariant, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
+ }
+
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task