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/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/CredentialDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor
new file mode 100644
index 00000000..47436c7e
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/CredentialDialog.razor
@@ -0,0 +1,109 @@
+@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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Change Password
+
+
+
+
+
+ Cancel
+ OK
+
+
+
+@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!;
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+
+ if (firstRender)
+ {
+
+ }
+ }
+
+ 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 = new ChangeCredentialRequest
+ {
+ CurrentSecret = _oldPassword!,
+ NewSecret = _newPassword!,
+ };
+
+ var result = await UAuthClient.Credentials.ChangeMyAsync(request);
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Password changed successfully", Severity.Success);
+ // Logout also if you want. Password change only revoke other open sessions, not the current one.
+ // await UAuthClient.Flows.LogoutAsync();
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "An error occurred while changing password", Severity.Error);
+ }
+ }
+
+ private string PasswordMatch(string arg) => _newPassword != arg ? "Passwords don't match" : string.Empty;
+
+ 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/IdentifierDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/IdentifierDialog.razor
index 6aba7ba5..dc5b27f1 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,17 +1,23 @@
-@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
-
+
- Identifiers
+
+ Identifiers
+
+
+
@@ -22,27 +28,27 @@
-
+
@if (context.Item.IsPrimary)
{
-
+
}
else
{
-
+
}
-
+
-
+
@@ -54,17 +60,29 @@
-
-
-
-
-
- Add
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
-
-
+
+
Cancel
@@ -73,8 +91,12 @@
@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,8 +104,6 @@
[Parameter]
public UAuthState AuthState { get; set; } = default!;
- private List _identifiers = new();
-
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
@@ -93,12 +113,77 @@
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
+ };
+
+ var res = await UAuthClient.Identifiers.GetMyIdentifiersAsync(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()
@@ -113,9 +198,10 @@
}
else
{
- Snackbar.Add("Failed to update identifier", Severity.Error);
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to update identifier", Severity.Error);
}
+ await ReloadAsync();
return DataGridEditFormAction.Close;
}
@@ -130,15 +216,15 @@
AddUserIdentifierRequest request = new()
{
Type = _newIdentifierType,
- Value = _newIdentifierValue
+ Value = _newIdentifierValue,
+ IsPrimary = _newIdentifierPrimary
};
var result = await UAuthClient.Identifiers.AddSelfAsync(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
@@ -147,37 +233,52 @@
}
}
- 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 };
+ var result = await UAuthClient.Identifiers.VerifySelfAsync(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?.Problem?.Detail ?? result?.Problem?.Title ?? "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 };
+ var result = await UAuthClient.Identifiers.SetPrimarySelfAsync(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?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to set primary identifier", Severity.Error);
}
}
@@ -188,8 +289,7 @@
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
@@ -205,8 +305,7 @@
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
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..9f89c886
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/PermissionDialog.razor
@@ -0,0 +1,155 @@
+@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 res = await UAuthClient.Authorization.SetPermissionsAsync(Role.Id, req);
+
+ if (!res.IsSuccess)
+ {
+ Snackbar.Add(res.Problem?.Title ?? "Failed to update permissions", Severity.Error);
+ return;
+ }
+
+ Role = (await UAuthClient.Authorization.QueryRolesAsync(new RoleQuery() { Search = Role.Name })).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..a6c7df97
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ProfileDialog.razor
@@ -0,0 +1,174 @@
+@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
+
+
+
+
+
+
+
+ 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!;
+
+ protected override async Task OnInitializedAsync()
+ {
+ var result = await UAuthClient.Users.GetMeAsync();
+
+ 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
+ };
+
+ var result = await UAuthClient.Users.UpdateMeAsync(request);
+
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Profile updated", Severity.Success);
+ MudDialog.Close(DialogResult.Ok(true));
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "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..2b94e413
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/ResetDialog.razor
@@ -0,0 +1,73 @@
+@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..540bc7ae
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/RoleDialog.razor
@@ -0,0 +1,236 @@
+@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 bool _reloadQueued;
+ 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()
+ {
+ if (_grid is null)
+ return;
+
+ await _grid.ReloadServerData();
+ }
+
+ 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..414e65e4
--- /dev/null
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/SessionDialog.razor
@@ -0,0 +1,407 @@
+@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
+
+
+
+ Identifier Management
+ User: @AuthState?.Identity?.DisplayName
+
+
+ @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
+ Logout Other Devices
+ Revoke All Devices
+ Revoke Other Devices
+
+
+
+ Identifiers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Id
+ @context.Item.ChainId
+
+
+
+ Created At
+ @context.Item.CreatedAt
+
+
+
+ Touch Count
+ @context.Item.TouchCount
+
+
+
+ Rotation Count
+ @context.Item.RotationCount
+
+
+
+
+
+
+
+
+ }
+
+
+ Cancel
+ OK
+
+
+
+@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!;
+
+ 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
+ };
+
+ var res = await UAuthClient.Sessions.GetMyChainsAsync();
+
+ 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()
+ {
+ var result = await UAuthClient.Flows.LogoutAllDevicesSelfAsync();
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Logged out of all devices.", Severity.Success);
+ Nav.NavigateTo("/login");
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "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?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task LogoutDeviceAsync(SessionChainId chainId)
+ {
+ LogoutDeviceSelfRequest request = new() { ChainId = chainId };
+ var result = await UAuthClient.Flows.LogoutDeviceSelfAsync(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?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task RevokeAllAsync()
+ {
+ var result = await UAuthClient.Sessions.RevokeAllMyChainsAsync();
+ if (result.IsSuccess)
+ {
+ Snackbar.Add("Logged out of all devices.", Severity.Success);
+ Nav.NavigateTo("/login");
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "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?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task RevokeChainAsync(SessionChainId chainId)
+ {
+ var result = await UAuthClient.Sessions.RevokeMyChainAsync(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?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to logout", Severity.Error);
+ }
+ }
+
+ private async Task ShowChainDetailsAsync(SessionChainId chainId)
+ {
+ var result = await UAuthClient.Sessions.GetMyChainDetailAsync(chainId);
+ if (result.IsSuccess)
+ {
+ _chainDetail = result.Value;
+ }
+ else
+ {
+ Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Failed to fetch chain details.", Severity.Error);
+ _chainDetail = null;
+ }
+ }
+
+ private void ClearDetail()
+ {
+ _chainDetail = null;
+ }
+
+ private void Submit() => MudDialog.Close(DialogResult.Ok(true));
+
+ private void Cancel() => MudDialog.Cancel();
+}
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..6cd64562 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,137 @@
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using System.Security.Claims
+@using CodeBeam.UltimateAuth.Core.Contracts
+@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)
+ {
+
+
+
+
+
+ Role Management
+
+
+
+
+
+ Assign Role
+
+
+
+ }
+
+
+
+
+
+
+
- @(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 +183,7 @@
Authenticated
- @(AuthState.IsAuthenticated ? "Yes" : "No")
+ @(AuthState?.IsAuthenticated == true ? "Yes" : "No")
@@ -134,6 +251,7 @@
Last Validated At
+ @* TODO: Validation call should update last validated at *@
@FormatLocalTime(AuthState?.LastValidatedAt)
@@ -186,202 +304,121 @@
-
-
-
-
- Session
+
+
+
+
+ @GetHealthText()
+
-
-
-
- Validate
-
-
-
-
-
- Manual Refresh
-
-
-
-
-
- Logout
-
-
-
+ Lifecycle
-
+
- 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..85e19dcf 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,10 @@
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.Components.Dialogs;
+using CodeBeam.UltimateAuth.Users.Contracts;
using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor;
using System.Security.Claims;
@@ -60,6 +62,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 +105,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,12 +154,36 @@ private string GetHealthText()
return utc?.ToLocalTime().ToString("dd MMM yyyy • HH:mm:ss");
}
- private async Task OpenIdentifierDialog()
+ private async Task OpenProfileDialog()
{
+ await DialogService.ShowAsync("Manage Profile", GetDialogParameters(), GetDialogOptions());
+ }
+ private async Task OpenIdentifierDialog()
+ {
await DialogService.ShowAsync("Manage Identifiers", GetDialogParameters(), GetDialogOptions());
}
+ private async Task OpenSessionDialog()
+ {
+ await DialogService.ShowAsync("Manage Sessions", GetDialogParameters(), GetDialogOptions());
+ }
+
+ private async Task OpenCredentialDialog()
+ {
+ await DialogService.ShowAsync("Session Diagnostics", GetDialogParameters(), GetDialogOptions());
+ }
+
+ private async Task OpenAccountStatusDialog()
+ {
+ await DialogService.ShowAsync("Manage Account", GetDialogParameters(), GetDialogOptions());
+ }
+
+ private async Task OpenRoleDialog()
+ {
+ await DialogService.ShowAsync("Role Management", GetDialogParameters(), GetDialogOptions());
+ }
+
private DialogOptions GetDialogOptions()
{
return new DialogOptions
@@ -168,13 +195,28 @@ private DialogOptions 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);
+ }
+ }
+
public override void Dispose()
{
base.Dispose();
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 77%
rename from src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs
rename to src/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs
index 7b106dd0..0dd58c6e 100644
--- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs
+++ b/src/CodeBeam.UltimateAuth.Client/AuthState/UAuthState.cs
@@ -1,6 +1,7 @@
using CodeBeam.UltimateAuth.Core.Contracts;
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 +28,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();
@@ -43,6 +46,32 @@ internal void ApplySnapshot(AuthStateSnapshot snapshot, DateTimeOffset validated
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 +92,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;
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/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/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..ba16baee
--- /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();
+ Task> GetUserRolesAsync(UserKey userKey);
+ 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..534efb0a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs
@@ -0,0 +1,23 @@
+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(ChangeCredentialRequest request);
+ Task RevokeMyAsync(RevokeCredentialRequest request);
+ Task> BeginResetMyAsync(BeginCredentialResetRequest request);
+ Task> CompleteResetMyAsync(CompleteCredentialResetRequest request);
+
+ Task> GetUserAsync(UserKey userKey);
+ Task> AddUserAsync(UserKey userKey, AddCredentialRequest request);
+ Task RevokeUserAsync(UserKey userKey, RevokeCredentialRequest request);
+ Task ActivateUserAsync(UserKey userKey);
+ Task> BeginResetUserAsync(UserKey userKey, BeginCredentialResetRequest request);
+ Task> CompleteResetUserAsync(UserKey userKey, CompleteCredentialResetRequest request);
+ Task DeleteUserAsync(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..ea759dff
--- /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(LogoutDeviceSelfRequest request);
+ Task LogoutOtherDevicesSelfAsync();
+ Task LogoutAllDevicesSelfAsync();
+ Task> LogoutDeviceAdminAsync(UserKey userKey, SessionChainId chainId);
+ Task LogoutOtherDevicesAdminAsync(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..da156b33
--- /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);
+ 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 95%
rename from src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs
rename to src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
index 88914fff..5c23dfe5 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
@@ -9,6 +9,7 @@ public interface IUserClient
Task> CreateAsync(CreateUserRequest request);
Task> ChangeStatusSelfAsync(ChangeUserStatusSelfRequest request);
Task> ChangeStatusAdminAsync(ChangeUserStatusAdminRequest request);
+ Task DeleteMeAsync();
Task> DeleteAsync(DeleteUserRequest request);
Task> GetMeAsync();
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..255e35ad 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;
}
@@ -38,9 +42,9 @@ public async Task> GetUserRolesAsync(UserKey user
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
});
@@ -48,13 +52,64 @@ public async Task AssignRoleAsync(UserKey userKey, string role)
return UAuthResultMapper.From(raw);
}
- 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
});
+ 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);
return UAuthResultMapper.From(raw);
}
+
+ 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..63aed3cc 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,11 +11,13 @@ 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;
}
@@ -32,28 +35,40 @@ public async Task> AddMyAsync(AddCredentialRequ
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($"/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($"/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($"/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($"/credentials/reset/complete"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.CredentialsChanged, _options.StateEvents.HandlingMode));
+ }
+ return UAuthResultMapper.FromJson(raw);
}
@@ -69,33 +84,33 @@ public async Task> AddUserAsync(UserKey userKey
return UAuthResultMapper.FromJson(raw);
}
- public async Task RevokeUserAsync(UserKey userKey, CredentialType type, RevokeCredentialRequest request)
+ public async Task RevokeUserAsync(UserKey userKey, RevokeCredentialRequest request)
{
- var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/revoke"), request);
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/revoke"), request);
return UAuthResultMapper.From(raw);
}
- public async Task ActivateUserAsync(UserKey userKey, CredentialType type)
+ public async Task ActivateUserAsync(UserKey userKey)
{
- var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/{type}/activate"));
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/activate"));
return UAuthResultMapper.From(raw);
}
- public async Task BeginResetUserAsync(UserKey userKey, CredentialType type, BeginCredentialResetRequest request)
+ public async Task> BeginResetUserAsync(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> CompleteResetUserAsync(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 DeleteUserAsync(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..6a1fe4b9 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,65 @@ public async Task CompletePkceLoginAsync(PkceLoginRequest request)
await _post.NavigateAsync(url, payload);
}
+ public async Task> LogoutDeviceSelfAsync(LogoutDeviceSelfRequest request)
+ {
+ var raw = await _post.SendJsonAsync(Url($"/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> LogoutDeviceAdminAsync(UserKey userKey, SessionChainId chainId)
+ {
+ LogoutDeviceAdminRequest request = new() { UserKey = userKey, ChainId = chainId };
+ var raw = await _post.SendJsonAsync(Url($"/admin/users/logout-device/{userKey.Value}"), request);
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task LogoutOtherDevicesSelfAsync()
+ {
+ var raw = await _post.SendJsonAsync(Url("/logout-others"));
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task LogoutOtherDevicesAdminAsync(LogoutOtherDevicesAdminRequest request)
+ {
+ var raw = await _post.SendJsonAsync(Url($"/admin/users/logout-others/{request.UserKey.Value}"), request);
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task LogoutAllDevicesSelfAsync()
+ {
+ var raw = await _post.SendJsonAsync(Url("/logout-all"));
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.LogoutVariant, _options.StateEvents.HandlingMode));
+ }
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task LogoutAllDevicesAdminAsync(UserKey userKey)
+ {
+ var raw = await _post.SendJsonAsync(Url($"/admin/users/logout-all/{userKey.Value}"));
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.LogoutVariant, _options.StateEvents.HandlingMode));
+ }
+ return UAuthResultMapper.From(raw);
+ }
+
+
private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl)
{
var hubLoginUrl = Url(_options.Endpoints.HubLoginPath);
@@ -242,9 +309,6 @@ private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifi
return _post.NavigateAsync(hubLoginUrl, data);
}
-
- // ---------------- PKCE CRYPTO ----------------
-
private static string CreateVerifier()
{
var bytes = RandomNumberGenerator.GetBytes(32);
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs
new file mode 100644
index 00000000..d181dbc9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthSessionClient.cs
@@ -0,0 +1,108 @@
+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;
+using Microsoft.Extensions.Options;
+
+namespace CodeBeam.UltimateAuth.Client.Services;
+
+internal sealed class UAuthSessionClient : ISessionClient
+{
+ private readonly IUAuthRequestClient _request;
+ private readonly UAuthClientOptions _options;
+ private readonly IUAuthClientEvents _events;
+
+ public UAuthSessionClient(IUAuthRequestClient request, IOptions options, IUAuthClientEvents events)
+ {
+ _request = request;
+ _options = options.Value;
+ _events = events;
+ }
+
+ private string Url(string path)
+ => UAuthUrlBuilder.Build(_options.Endpoints.BasePath, path, _options.MultiTenant);
+
+
+ public async Task>> GetMyChainsAsync(PageRequest? request = null)
+ {
+ request ??= new PageRequest();
+ var raw = await _request.SendJsonAsync(Url("/session/me/chains"), request);
+ return UAuthResultMapper.FromJson>(raw);
+ }
+
+ public async Task> GetMyChainDetailAsync(SessionChainId chainId)
+ {
+ var raw = await _request.SendFormAsync(Url($"/session/me/chains/{chainId}"));
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task> RevokeMyChainAsync(SessionChainId chainId)
+ {
+ var raw = await _request.SendJsonAsync(Url($"/session/me/chains/{chainId}/revoke"));
+ var result = UAuthResultMapper.FromJson(raw);
+
+ if (result.Value?.CurrentChain == true)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.SessionRevoked, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
+ }
+
+ public async Task RevokeMyOtherChainsAsync()
+ {
+ var raw = await _request.SendFormAsync(Url("/session/me/revoke-others"));
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task RevokeAllMyChainsAsync()
+ {
+ var raw = await _request.SendFormAsync(Url("/session/me/revoke-all"));
+ var result = UAuthResultMapper.From(raw);
+
+ if (result.IsSuccess)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.SessionRevoked, _options.StateEvents.HandlingMode));
+ }
+
+ return result;
+ }
+
+
+ public async Task>> GetUserChainsAsync(UserKey userKey)
+ {
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/sessions/chains"));
+ return UAuthResultMapper.FromJson>(raw);
+ }
+
+ public async Task> GetUserChainDetailAsync(UserKey userKey, SessionChainId chainId)
+ {
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/sessions/chains/{chainId}"));
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task RevokeUserSessionAsync(UserKey userKey, AuthSessionId sessionId)
+ {
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/sessions/{sessionId}/revoke"));
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task RevokeUserChainAsync(UserKey userKey, SessionChainId chainId)
+ {
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/sessions/chains/{chainId}/revoke"));
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task RevokeUserRootAsync(UserKey userKey)
+ {
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/sessions/revoke-root"));
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task RevokeAllUserChainsAsync(UserKey userKey)
+ {
+ var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/sessions/revoke-all"));
+ return UAuthResultMapper.From(raw);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs
index 3f2c426b..9fd9fb09 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.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,11 +11,13 @@ namespace CodeBeam.UltimateAuth.Client.Services;
internal sealed class UAuthUserClient : IUserClient
{
private readonly IUAuthRequestClient _request;
+ private readonly IUAuthClientEvents _events;
private readonly UAuthClientOptions _options;
- public UAuthUserClient(IUAuthRequestClient request, IOptions options)
+ public UAuthUserClient(IUAuthRequestClient request, IUAuthClientEvents events, IOptions options)
{
_request = request;
+ _events = events;
_options = options.Value;
}
@@ -29,6 +32,20 @@ public async Task> GetMeAsync()
public async Task UpdateMeAsync(UpdateProfileRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/update"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgs(UAuthStateEvent.ProfileChanged, _options.StateEvents.HandlingMode, request));
+ }
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task DeleteMeAsync()
+ {
+ var raw = await _request.SendJsonAsync(Url("/users/me/delete"));
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.UserDeleted, UAuthStateEventHandlingMode.Patch));
+ }
return UAuthResultMapper.From(raw);
}
@@ -41,6 +58,10 @@ public async Task> CreateAsync(CreateUserRequest r
public async Task> ChangeStatusSelfAsync(ChangeUserStatusSelfRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/status"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgs(UAuthStateEvent.ProfileChanged, _options.StateEvents.HandlingMode, request));
+ }
return UAuthResultMapper.FromJson(raw);
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs
index 7a814c6c..b4ccfa64 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.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,62 +11,90 @@ namespace CodeBeam.UltimateAuth.Client.Services;
internal class UAuthUserIdentifierClient : IUserIdentifierClient
{
private readonly IUAuthRequestClient _request;
+ private readonly IUAuthClientEvents _events;
private readonly UAuthClientOptions _options;
- public UAuthUserIdentifierClient(IUAuthRequestClient request, IOptions options)
+ public UAuthUserIdentifierClient(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>> GetMyIdentifiersAsync()
+ public async Task>> GetMyIdentifiersAsync(PageRequest? request = null)
{
- var raw = await _request.SendFormAsync(Url("/users/me/identifiers/get"));
- return UAuthResultMapper.FromJson>(raw);
+ request ??= new PageRequest();
+ var raw = await _request.SendJsonAsync(Url("/users/me/identifiers/get"), request);
+ return UAuthResultMapper.FromJson>(raw);
}
public async Task AddSelfAsync(AddUserIdentifierRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/identifiers/add"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgs(UAuthStateEvent.IdentifiersChanged, _options.StateEvents.HandlingMode, request));
+ }
return UAuthResultMapper.From(raw);
}
public async Task UpdateSelfAsync(UpdateUserIdentifierRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/identifiers/update"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgs(UAuthStateEvent.IdentifiersChanged, _options.StateEvents.HandlingMode, request));
+ }
return UAuthResultMapper.From(raw);
}
public async Task SetPrimarySelfAsync(SetPrimaryUserIdentifierRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/identifiers/set-primary"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.IdentifiersChanged, _options.StateEvents.HandlingMode));
+ }
return UAuthResultMapper.From(raw);
}
public async Task UnsetPrimarySelfAsync(UnsetPrimaryUserIdentifierRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/identifiers/unset-primary"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.IdentifiersChanged, _options.StateEvents.HandlingMode));
+ }
return UAuthResultMapper.From(raw);
}
public async Task VerifySelfAsync(VerifyUserIdentifierRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/identifiers/verify"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.IdentifiersChanged, _options.StateEvents.HandlingMode));
+ }
return UAuthResultMapper.From(raw);
}
public async Task DeleteSelfAsync(DeleteUserIdentifierRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/me/identifiers/delete"), request);
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(new UAuthStateEventArgsEmpty(UAuthStateEvent.IdentifiersChanged, _options.StateEvents.HandlingMode));
+ }
return UAuthResultMapper.From(raw);
}
- public async Task>> GetUserIdentifiersAsync(UserKey userKey)
+ public async Task>> GetUserIdentifiersAsync(UserKey userKey, PageRequest? request = null)
{
- var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey.Value}/identifiers/get"));
- return UAuthResultMapper.FromJson>(raw);
+ request ??= new PageRequest();
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey.Value}/identifiers/get"), request);
+ return UAuthResultMapper.FromJson>(raw);
}
public async Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request)
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/IEntitySnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/IEntitySnapshot.cs
new file mode 100644
index 00000000..12995e3e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/IEntitySnapshot.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions;
+
+public interface IEntitySnapshot
+{
+ T Snapshot();
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/IVersionedEntity.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/IVersionedEntity.cs
new file mode 100644
index 00000000..893206e2
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Entity/IVersionedEntity.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions;
+
+public interface IVersionedEntity
+{
+ long Version { get; set; }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs
index 71e7a186..0d40c708 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs
@@ -4,6 +4,7 @@
/// Provides an abstracted time source for the system.
/// Used to improve testability and ensure consistent time handling.
///
+// TODO: Add UnixTimeSeconds, TimeZone-aware Now, etc. if needed.
public interface IClock
{
DateTimeOffset UtcNow { get; }
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs
index 8112e451..1096f980 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs
@@ -7,5 +7,5 @@
public interface ITokenHasher
{
string Hash(string plaintext);
- bool Verify(string plaintext, string hash);
+ bool Verify(string hash, string plaintext);
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/INumericCodeGenerator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/INumericCodeGenerator.cs
new file mode 100644
index 00000000..73b4cfc8
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/INumericCodeGenerator.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions;
+
+public interface INumericCodeGenerator
+{
+ string Generate(int digits = 6);
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs
index f78e4806..b427fb04 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs
@@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;
public interface ISessionIssuer
{
- Task IssueLoginSessionAsync(AuthenticatedSessionContext context, CancellationToken cancellationToken = default);
+ Task