diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/ILabAdminGuard.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/ILabAdminGuard.cs new file mode 100644 index 00000000..dae84f5a --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/ILabAdminGuard.cs @@ -0,0 +1,9 @@ +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.IdentityLab.Authorization; + +public interface ILabAdminGuard +{ + Task IsAdminAsync(AccessContext context, CancellationToken ct = default); + Task EnsureAdminAsync(AccessContext context, CancellationToken ct = default); +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAccessContextProvider.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAccessContextProvider.cs new file mode 100644 index 00000000..dce46417 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAccessContextProvider.cs @@ -0,0 +1,14 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Auth; + +namespace CodeBeam.UltimateAuth.IdentityLab.Authorization; + +public sealed class LabAccessContextProvider(IAuthFlow authFlow, IAccessContextFactory contextFactory) +{ + public async Task CreateAsync(string action, string resource, CancellationToken cancellationToken = default) + { + var flow = await authFlow.BeginAsync(AuthFlowType.ValidateSession, cancellationToken); + return await contextFactory.CreateAsync(flow, action, resource, ct: cancellationToken); + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminAuthorizationFilter.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminAuthorizationFilter.cs new file mode 100644 index 00000000..220e5f06 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminAuthorizationFilter.cs @@ -0,0 +1,27 @@ +using CodeBeam.UltimateAuth.IdentityLab.Authorization; + +namespace CodeBeam.UltimateAuth.IdentityLab.Endpoints; + +public sealed class LabAdminAuthorizationFilter(LabAccessContextProvider contextProvider, ILabAdminGuard adminGuard) : IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext invocation, + EndpointFilterDelegate next) + { + var context = await contextProvider.CreateAsync( + "identitylab.admin", + "identitylab", + invocation.HttpContext.RequestAborted); + + if (!context.IsAuthenticated || context.ActorUserKey is null) + return Results.Unauthorized(); + + if (!await adminGuard.IsAdminAsync( + context, + invocation.HttpContext.RequestAborted)) + { + return Results.Forbid(); + } + + return await next(invocation); + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminGuard.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminGuard.cs new file mode 100644 index 00000000..3be4b207 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminGuard.cs @@ -0,0 +1,41 @@ +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.IdentityLab.Authorization; + +public sealed class LabAdminGuard(IRoleStoreFactory roleStoreFactory, IUserRoleStoreFactory userRoleStoreFactory) : ILabAdminGuard +{ + public async Task IsAdminAsync(AccessContext context, CancellationToken ct = default) + { + if (!context.IsAuthenticated || + context.IsSystemActor || + context.ActorUserKey is null || + context.ActorTenant.IsSystem || + context.ActorTenant.IsUnresolved || + context.IsCrossTenant) + { + return false; + } + + var roleStore = roleStoreFactory.Create(context.ActorTenant); + + var adminRole = await roleStore.GetByNameAsync(LabRoles.Admin.ToUpperInvariant(), ct); + + if (adminRole is null) + return false; + + var userRoleStore = userRoleStoreFactory.Create(context.ActorTenant); + + var assignments = await userRoleStore.GetAssignmentsAsync(context.ActorUserKey.Value, ct); + + return assignments.Any(x => x.RoleId == adminRole.Id); + } + + public async Task EnsureAdminAsync(AccessContext context, CancellationToken ct = default) + { + if (!await IsAdminAsync(context, ct)) + { + throw new UnauthorizedAccessException("LabAdmin permission is required."); + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminIdentity.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminIdentity.cs new file mode 100644 index 00000000..0476d3e3 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminIdentity.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.IdentityLab.Authorization; + +public static class LabAdminIdentity +{ + public static readonly UserKey UserKey = UserKey.FromGuid(Guid.Parse("1a6d0c8e-8b93-4c74-a1c9-3d5f9276b410")); +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminOptions.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminOptions.cs new file mode 100644 index 00000000..d3dc0385 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabAdminOptions.cs @@ -0,0 +1,11 @@ +namespace CodeBeam.UltimateAuth.IdentityLab.Authorization; + +public sealed class LabAdminOptions +{ + public const string SectionName = "LabAdmin"; + + public string Username { get; set; } = "Ladmin"; + public string Email { get; set; } = string.Empty; + public string DisplayName { get; set; } = "Lab Administrator"; + public string Password { get; set; } = string.Empty; +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabRoles.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabRoles.cs new file mode 100644 index 00000000..16c14277 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Authorization/LabRoles.cs @@ -0,0 +1,15 @@ +namespace CodeBeam.UltimateAuth.IdentityLab.Authorization; + +public static class LabRoles +{ + public const string Admin = "Ladmin"; + public const string Owner = "Owner"; + public const string User = "User"; + + public static readonly IReadOnlyList SystemRoles = + [ + Admin, + Owner, + User + ]; +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor index 2f518741..5bab31b3 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor @@ -1,6 +1,13 @@ @inherits LayoutComponentBase @implements IDisposable +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Authorization +@using CodeBeam.UltimateAuth.IdentityLab.Contracts +@using CodeBeam.UltimateAuth.IdentityLab.Services @using CodeBeam.UltimateAuth.Sample.Shared.Infrastructure +@inject LabDashboardService DashboardService +@inject IUAuthClient UAuthClient @inject DarkModeManager DarkModeManager @@ -19,7 +26,7 @@ - + Identity Lab @@ -33,7 +40,7 @@ Delay="300"> @@ -45,11 +52,59 @@ Color="Color.Inherit" /> - - - + + + + + @AvatarInitials + + + + +
+ + @AvatarInitials + + +
+
+ @DisplayName +
+ +
+ @@ @UserName +
+
+
+ + + + + My Identity + + + + My Sessions + + + + + + Sign Out + +
+
@@ -67,20 +122,24 @@
- My Sandbox - Identity environment + @(_sandbox?.SandboxName ?? "My Sandbox") + + @(_sandbox is null ? "Identity environment" : $"{_sandbox.MemberCount} / {_sandbox.MaxUsers} users") +
- +
WORKSPACE
- + + + Platform Administration + + + + Overview @@ -131,6 +190,12 @@ Settings } + + + + Sign Out + +
@@ -144,29 +209,107 @@ + @if (AuthState?.Identity?.UserStatus == UserStatus.SelfSuspended) + { + + Account Suspended + + + Your account is currently self-suspended. + Most account operations are restricted. + Reactivate your account from + My Identity → Account Management. + + + } + else if (AuthState?.Identity?.UserStatus == UserStatus.Suspended || AuthState?.Identity?.UserStatus == UserStatus.Locked) + { + + + Your account has been suspended + + + Your account has been suspended/locked by an administrator. + Some actions are unavailable. + Please contact your system administrator to restore access. + + + } @Body @code { + [CascadingParameter] + private UAuthState? AuthState { get; set; } + + private string UserName => AuthState?.Identity?.PrimaryUserName ?? "User"; + private string DisplayName => UserName; + private string AvatarInitials => GetInitials(DisplayName); + private LabDashboardSnapshot? _sandbox; + private UserKey? _loadedUserKey; private bool _drawerOpen = true; - // UI preview only. Replace with sandbox-scoped authorization. - private bool ShowManagement => true; + private bool ShowManagement => + AuthState?.IsInRole(LabRoles.Admin) == true || + (AuthState?.IsInRole(LabRoles.Owner) == true && + _sandbox?.IsOwner == true); protected override void OnInitialized() { DarkModeManager.Changed += OnThemeChanged; } + protected override async Task OnParametersSetAsync() + { + var userKey = AuthState?.Identity?.UserKey; + + if (userKey is null) + { + _sandbox = null; + _loadedUserKey = null; + return; + } + + if (_loadedUserKey == userKey) + return; + + _loadedUserKey = userKey; + + _sandbox = await DashboardService.GetAsync(userKey.Value); + } + private void OnThemeChanged() { _ = InvokeAsync(StateHasChanged); } + private async Task LogoutAsync() + { + await UAuthClient.Flows.LogoutAsync(); + } + public void Dispose() { DarkModeManager.Changed -= OnThemeChanged; } + + private static string GetInitials(string? displayName) + { + if (string.IsNullOrWhiteSpace(displayName)) + return "?"; + + var parts = displayName.Trim() + .Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var initials = parts.Length > 1 + ? $"{parts[0][0]}{parts[^1][0]}" + : new string(parts[0].Take(2).ToArray()); + + return initials.ToUpperInvariant(); + } } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css index b0fb97c9..00097497 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css @@ -134,6 +134,58 @@ display: none; } +.lab-profile-avatar { + cursor: pointer; + font-weight: 700; + font-size: 0.85rem; + letter-spacing: 0.025em; + user-select: none; + transition: transform 150ms ease, opacity 150ms ease; +} + + .lab-profile-avatar:hover { + transform: scale(1.06); + opacity: 0.9; + } + +.lab-profile-menu-header { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px 8px; + min-width: 230px; +} + +.lab-profile-menu-avatar { + flex-shrink: 0; + font-weight: 700; + font-size: 0.85rem; +} + +.lab-profile-menu-user { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.lab-profile-menu-name { + font-size: 0.9rem; + font-weight: 600; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.lab-profile-menu-username { + font-size: 0.75rem; + opacity: 0.65; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + @media (max-width: 960px) { .lab-mobile-menu { display: inline-flex; diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/AdminPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/AdminPage.razor new file mode 100644 index 00000000..cb5d3771 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/AdminPage.razor @@ -0,0 +1,350 @@ +@page "/admin" +@layout DashboardLayout +@attribute [UAuthAuthorize(Roles = "Ladmin")] + +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Contracts +@using CodeBeam.UltimateAuth.IdentityLab.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Services +@using CodeBeam.UltimateAuth.IdentityLab.Components.Pages.Dialogs + +@inject LabAdminService AdminService +@inject IDialogService DialogService + +Administration · Identity Lab + + + + + + + + PLATFORM ADMINISTRATION + + + + Sandbox Management + + + + Monitor and manage all Identity Lab environments. + + + + @if (_loading) + { + + } + else if (!string.IsNullOrWhiteSpace(_error)) + { + + @_error + + } + else if (_snapshot is not null) + { + + + + + + Total Sandboxes + + + @_snapshot.TotalSandboxes + + + + + + + + Active Sandboxes + + + @_snapshot.ActiveSandboxes + + + + + + + + Suspended + + + @_snapshot.SuspendedSandboxes + + + + + + + + Total Members + + + @_snapshot.TotalMembers + + + + + + + + + + + + + + All Sandboxes + + + + + + + + + + + + + + + + All statuses + + + + Active + + + + Passive + + + + Suspended + + + + + + + + + + + Sandbox + Owner + Members + Status + Actions + + + + + + @context.Name + + + + + @context.OwnerName + + + + @context.MemberCount / @context.MaxUsers + + + + + @context.Status + + + + + + + + + + + + + No sandboxes found. + + + + + + + + + + + + + } + + + + + +@code { + [CascadingParameter] + private UAuthState? AuthState { get; set; } + + private MudTable? _table; + private AdminDashboardSnapshot? _snapshot; + private UserKey? _loadedUserKey; + private bool _loading; + private string? _error; + + private string _search = string.Empty; + private string _statusFilter = "all"; + + protected override async Task OnParametersSetAsync() + { + var userKey = AuthState?.Identity?.UserKey; + + if (userKey is null) + { + _loadedUserKey = null; + _snapshot = null; + return; + } + + if (_loadedUserKey == userKey) + return; + + await LoadAsync(); + } + + private async Task LoadAsync() + { + var userKey = AuthState?.Identity?.UserKey; + + if (userKey is null) + return; + + _loading = true; + _error = null; + + try + { + _snapshot = await AdminService.GetStatisticsAsync(userKey.Value); + _loadedUserKey = userKey; + } + catch (UnauthorizedAccessException) + { + _snapshot = null; + _error = "Platform administrator access is required."; + } + catch (Exception) + { + _snapshot = null; + _error = "Unable to load platform administration data."; + } + finally + { + _loading = false; + } + } + + private async Task> LoadSandboxTableAsync(TableState state, CancellationToken ct) + { + var actor = AuthState?.Identity?.UserKey; + + if (actor is null) + { + return new TableData + { + Items = [], + TotalItems = 0 + }; + } + + var result = await AdminService.QuerySandboxesAsync(actor.Value, + new AdminSandboxQuery( + _search, + _statusFilter == "all" ? null : _statusFilter, + state.Page, + state.PageSize), + ct); + + return new TableData + { + Items = result.Items, + TotalItems = result.TotalCount + }; + } + + private async Task RefreshTableAsync() + { + if (_table is not null) + _table.NavigateTo(0); + } + + private async Task OpenManageAsync(AdminSandboxItem sandbox) + { + var actorUserKey = AuthState?.Identity?.UserKey; + + if (actorUserKey is null) + return; + + var parameters = new DialogParameters + { + { x => x.Sandbox, sandbox }, + { x => x.ActorUserKey, actorUserKey.Value } + }; + + var dialog = await DialogService + .ShowAsync( + "Manage Sandbox", + parameters, + UAuthDialog.GetDialogOptions(MaxWidth.Medium)); + + var result = await dialog.Result; + + if (result?.Canceled == false) + await LoadAsync(); + } + + private static Color GetStatusColor(SandboxStatus status) => + status switch + { + SandboxStatus.Active => Color.Success, + SandboxStatus.Passive => Color.Default, + SandboxStatus.Suspended => Color.Warning, + _ => Color.Default + }; +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/ActivityPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/ActivityPage.razor index 136e1bd9..da663b83 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/ActivityPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/ActivityPage.razor @@ -1,9 +1,14 @@ @page "/dashboard/activity" +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Services @layout DashboardLayout -@* TODO: Enable sandbox-scoped authorization. @attribute [UAuthAuthorize] -*@ +@inherits UAuthComponentBase + +@inject SandboxService SandboxService +@inject ActivityLogService ActivityLogService Activity · Identity Lab @@ -16,14 +21,6 @@

Activity

Review identity, session, and authorization activity within your sandbox.

- - - Demo Preview -
@@ -37,15 +34,15 @@
Actors - @_events.Select(x => x.Actor).Distinct().Count() + @_events.Select(x => x.ActorUserKey).Distinct().Count() Example users
- Security Events - @_events.Count(x => x.Category == ActivityCategory.Security) - Illustrative security activity + Login Events + @_events.Count(x => x.EventType == ActivityEvents.Login) + Security activity
@@ -76,18 +73,6 @@ Margin="Margin.Dense" Class="lab-activity-search" /> - - All categories - Identity - Session - Authorization - Security - - - @@ -109,61 +95,51 @@ Event Actor - Category + Target Date - Details +
- +
- @context.Title - @context.Description + @GetEventTitle(context.EventType)
- @context.Actor + + @context.ActorUserKey + - - - @context.Category - + + @(context.TargetUserKey ?? "—") - @context.Timestamp.ToString("MMM dd, yyyy") + @context.CreatedAt.ToLocalTime().ToString("MMM dd, yyyy HH:mm") - -
- -
-
- - No matching events -

Try adjusting your filters.

+ + No activity recorded +

Activity will appear here as users interact with the sandbox.

@@ -174,120 +150,100 @@
- @if (_selectedEvent is not null) - { -
-
-
-

Event Details

-

Example activity record

-
- -
- -
-
Event@_selectedEvent.Title
-
Actor@_selectedEvent.Actor
-
Category@_selectedEvent.Category
-
Timestamp (UTC)@_selectedEvent.Timestamp.ToString("yyyy-MM-dd HH:mm")
-
- -

- @_selectedEvent.Description -

-
- } - - Activity records shown here are illustrative. Production activity - will require explicit event recording, sandbox isolation, and - appropriate data retention rules. + Activity records are temporarily stored by Identity Lab. + This lightweight activity history will be replaced by UltimateAuth's built-in audit system once it becomes available. @code { + private readonly List _events = []; + private string _search = string.Empty; - private string _category = "all"; private string _period = "all"; - private ActivityPreview? _selectedEvent; + private bool _loading; + private string? _error; - private static readonly DateTime PreviewDate = - new(2026, 10, 21, 12, 0, 0, DateTimeKind.Utc); + private async Task InitializeActivityAsync(UserKey currentUserKey) + { + var sandbox = await SandboxService.GetByUserAsync(currentUserKey); - private readonly List _events = - [ - new("User Created", "A new member was added to the sandbox.", - "alex", ActivityCategory.Identity, PreviewDate.AddHours(-2)), + if (sandbox is null) + { + _error = "You are not a member of any sandbox."; + return; + } - new("Session Revoked", "A remote session was revoked.", - "taylor", ActivityCategory.Session, PreviewDate.AddHours(-5)), + if (sandbox.Status != SandboxStatus.Active) + { + _error = "Your sandbox is not active."; + return; + } - new("Role Updated", "A sandbox role configuration was changed.", - "alex", ActivityCategory.Authorization, PreviewDate.AddDays(-1)), + await LoadActivityAsync(currentUserKey); + } - new("Password Changed", "A member updated their credential.", - "jordan", ActivityCategory.Security, PreviewDate.AddDays(-2)), + private async Task LoadActivityAsync(UserKey currentUserKey) + { + _loading = true; + _error = null; - new("User Disabled", "A member account was disabled.", - "alex", ActivityCategory.Identity, PreviewDate.AddDays(-4)), + try + { + var events = await ActivityLogService.GetRecentAsync( + currentUserKey, + take: 100); - new("Session Created", "A new authenticated session was established.", - "taylor", ActivityCategory.Session, PreviewDate.AddDays(-9)) - ]; + _events.Clear(); + _events.AddRange(events); + } + catch (Exception) + { + _error = "Unable to load activity records."; + } + finally + { + _loading = false; + } + } - private IEnumerable FilteredEvents => + private IEnumerable FilteredEvents => _events .Where(x => (string.IsNullOrWhiteSpace(_search) || - x.Title.Contains(_search, StringComparison.OrdinalIgnoreCase) || - x.Description.Contains(_search, StringComparison.OrdinalIgnoreCase) || - x.Actor.Contains(_search, StringComparison.OrdinalIgnoreCase)) && - (_category == "all" || - x.Category.ToString().Equals(_category, StringComparison.OrdinalIgnoreCase)) && + x.EventType.Contains(_search, StringComparison.OrdinalIgnoreCase) || + x.ActorUserKey.Contains(_search, StringComparison.OrdinalIgnoreCase) || + (x.TargetUserKey?.Contains( + _search, + StringComparison.OrdinalIgnoreCase) ?? false)) && (_period == "all" || (int.TryParse(_period, out var days) && - x.Timestamp >= PreviewDate.AddDays(-days)))) - .OrderByDescending(x => x.Timestamp); + x.CreatedAt >= DateTimeOffset.UtcNow.AddDays(-days)))) + .OrderByDescending(x => x.CreatedAt); - private void SelectEvent(ActivityPreview item) => _selectedEvent = item; - - private static string GetIcon(ActivityCategory category) => category switch + private static string GetEventTitle(string eventType) => eventType switch { - ActivityCategory.Identity => Icons.Material.Filled.PersonOutline, - ActivityCategory.Session => Icons.Material.Filled.Devices, - ActivityCategory.Authorization => Icons.Material.Filled.Shield, - ActivityCategory.Security => Icons.Material.Filled.Security, - _ => Icons.Material.Outlined.Info + "user.login" => "User Logged In", + "user.logout" => "User Logged Out", + "user.created" => "User Created", + "user.updated" => "User Updated", + "user.deleted" => "User Deleted", + _ => eventType }; - private static Color GetColor(ActivityCategory category) => category switch + private static string GetEventIcon(string eventType) => eventType switch { - ActivityCategory.Identity => Color.Primary, - ActivityCategory.Session => Color.Info, - ActivityCategory.Authorization => Color.Secondary, - ActivityCategory.Security => Color.Warning, - _ => Color.Default + "user.login" => Icons.Material.Filled.Login, + "user.logout" => Icons.Material.Filled.Logout, + "user.created" => Icons.Material.Filled.PersonAdd, + "user.updated" => Icons.Material.Filled.ManageAccounts, + "user.deleted" => Icons.Material.Filled.PersonRemove, + _ => Icons.Material.Filled.History }; - - private enum ActivityCategory - { - Identity, - Session, - Authorization, - Security - } - - private sealed record ActivityPreview( - string Title, - string Description, - string Actor, - ActivityCategory Category, - DateTime Timestamp); } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/AuthorizationPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/AuthorizationPage.razor index a002051b..f57c2d66 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/AuthorizationPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/AuthorizationPage.razor @@ -1,10 +1,14 @@ @page "/dashboard/authorization" @layout DashboardLayout -@inject IDialogService DialogService -@* TODO: Enable sandbox-scoped authorization. @attribute [UAuthAuthorize] -*@ + +@using CodeBeam.UltimateAuth.Authorization.Contracts +@using CodeBeam.UltimateAuth.Core.Contracts + +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService Authorization · Identity Lab @@ -15,10 +19,7 @@
SANDBOX MANAGEMENT

Authorization

-

- Explore roles, permission assignments, - and access control within your sandbox. -

+

Explore roles, permission assignments and access control within your sandbox.

Roles - @_roles.Count - Example sandbox roles + @_totalRoles + Total roles
@@ -45,17 +46,17 @@ Permissions @_permissions.Count - Illustrative permissions + Available permissions
-
+ @*
Role Assignments 3 Example assignments -
+
*@
@@ -65,14 +66,6 @@ Select a role to inspect its assigned permissions.

- - - Demo Preview -
@@ -91,88 +84,124 @@ @foreach (var role in _roles) { } + + @if (!_loading && _roles.Count == 0) + { +
+ No roles found. +
+ } +
+ +
+
+
-

@SelectedRole.Name Permissions

-

Permissions assigned to this example role.

+

@(SelectedRole?.Name ?? "Role") Permissions

+

Permissions assigned to the selected role.

+ Disabled="@(_loading || SelectedRole is null)" + OnClick="EditPermissionsAsync"> Edit
- + @if (SelectedRole is not null) + { + + +
+ + @foreach (var permission in FilteredPermissions) + { + var granted = GrantedPermissions.Contains(permission); -
- @foreach (var permission in FilteredPermissions) - { -
- - - +
+ + + + + +
+ @permission +
+ + + @(granted ? "Granted" : "Not granted") + -
- @permission.Name - @permission.Description
+ } - - @(SelectedRole.PermissionIds.Contains(permission.Id) - ? "Granted" - : "Not granted") - -
- } + @if (!FilteredPermissions.Any()) + { +
+ No permissions match your search. +
+ } +
+ } + else + { +
+ Select a role to inspect its permissions. +
+ } - @if (!FilteredPermissions.Any()) - { -
- No permissions match your search. -
- } -
@@ -193,76 +222,159 @@

Access Request

-

Configure an example authorization request.

+

Evaluate authorization for your current session.

+
- - @foreach (var role in _roles) - { - - @role.Name - - } - + @bind-Value="_playgroundAction"> + @foreach (var permission in _permissions) { - - @permission.Name + + @permission } + + + + + + Disabled="@(_evaluating || string.IsNullOrWhiteSpace(_playgroundAction) || string.IsNullOrWhiteSpace(_playgroundResource))" + OnClick="EvaluatePolicyAsync"> + + @if (_evaluating) + { + + } + Evaluate Policy +
+

Evaluation Result

-

Live authorization decisions will appear here.

+

Server-side authorization decision.

+
-
- - - - Ready to evaluate -

- Connect UltimateAuth authorization to inspect - actual access decisions for sandbox users. -

-
+ @if (_evaluationResult is null) + { +
+ + + + + Ready to evaluate + +

+ Select an action and resource to evaluate + access using your current authentication context. +

+
+ } + else + { + + + + @(_evaluationResult.IsAllowed + ? "Access Granted" + : "Access Denied") + + +
+ @(_evaluationResult.IsAllowed + ? "The current session is authorized to perform this action." + : "The current session is not authorized to perform this action.") +
+ +
+ +
+ +
+ Action + @_evaluatedAction +
+ +
+ Resource + @_evaluatedResource +
+ +
+ Decision + + @(_evaluationResult.IsAllowed ? "Allow" : "Deny") + +
+ +
+ Reauthentication + + @(_evaluationResult.RequiresReauthentication + ? "Required" + : "Not required") + +
+ + @if (!string.IsNullOrWhiteSpace(_evaluationResult.DenyReason)) + { +
+ Denial Reason + @_evaluationResult.DenyReason +
+ } + +
+ } - A role's permission list alone does not guarantee - an authorization decision. Policies and context - may also affect the result. + Dense="true" + Class="mt-4"> + Authorization is evaluated by UltimateAuth on the server. + The result reflects your current authentication context, + not a simulated role. +
@@ -271,81 +383,197 @@ @code { + private const int PageSize = 10; + + private readonly List _roles = []; + private readonly List _permissions = []; + + private RoleId? _selectedRoleId; private string _permissionSearch = string.Empty; - private string _playgroundRoleId = "owner"; - private string _playgroundPermissionId = "users.query"; + + private int _page = 1; + private int _totalRoles; + + private bool _loading; + private string? _error; + + private string? _playgroundAction; + private string _playgroundResource = "users"; + private string? _playgroundResourceId; + + private bool _evaluating; + + private UltimateAuth.Authorization.Contracts.AuthorizationResult? _evaluationResult; + + private string? _evaluatedAction; + private string? _evaluatedResource; + + private int TotalPages => + Math.Max(1, (_totalRoles + PageSize - 1) / PageSize); + + private RoleInfo? SelectedRole => + _roles.FirstOrDefault(x => x.Id == _selectedRoleId); + + private IEnumerable FilteredPermissions => + _permissions.Where(x => + string.IsNullOrWhiteSpace(_permissionSearch) || + x.Contains(_permissionSearch, StringComparison.OrdinalIgnoreCase)); + + private HashSet GrantedPermissions => + SelectedRole is null + ? [] + : PermissionExpander + .Expand( + SelectedRole.Permissions, + UAuthPermissionCatalog.GetAdminPermissions()) + .Select(x => x.Value) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + protected override async Task OnInitializedAsync() + { + _permissions.AddRange( + UAuthPermissionCatalog.GetAdminPermissions() + .OrderBy(x => x)); + + await LoadRolesAsync(); + } + + private async Task LoadRolesAsync() + { + if (_loading) + return; + + _loading = true; + _error = null; + + try + { + var result = await UAuthClient.Authorization.QueryRolesAsync( + new RoleQuery + { + PageNumber = _page, + PageSize = PageSize, + SortBy = nameof(RoleInfo.CreatedAt), + Descending = true + }); + + if (!result.IsSuccess || result.Value is null) + { + _error = result.ErrorText ?? "Unable to load roles."; + return; + } + + _roles.Clear(); + _roles.AddRange(result.Value.Items); + _totalRoles = result.Value.TotalCount; + + if (SelectedRole is null) + _selectedRoleId = _roles.FirstOrDefault()?.Id; + } + catch (Exception) + { + _error = "Authorization service is unavailable."; + } + finally + { + _loading = false; + } + } + + private async Task OnPageChangedAsync(int page) + { + _page = page; + await LoadRolesAsync(); + } + + private void SelectRole(RoleInfo role) + { + _selectedRoleId = role.Id; + } private async Task OpenRoleManagementAsync() { var parameters = new DialogParameters { - { x => x.PreviewMode, true } + { x => x.PreviewMode, false } }; - await DialogService.ShowAsync( + var dialog = await DialogService.ShowAsync( "Role Management", parameters, UAuthDialog.GetDialogOptions(MaxWidth.Large)); - } - private readonly List _permissions = - [ - new("users.query", "users.query.self", "View own user information"), - new("users.manage", "users.update.admin", "Manage sandbox users"), - new("sessions.query", "sessions.query.self", "Inspect own sessions"), - new("sessions.revoke", "sessions.revoke.self", "Revoke own sessions"), - new("roles.manage", "roles.update.admin", "Manage role assignments"), - new("credentials.update", "credentials.update.self", "Update own credentials") - ]; - - private readonly List _roles = - [ - new( - "owner", - "Owner", - "Sandbox administration", - 1, - true, - ["users.query", "users.manage", "sessions.query", - "sessions.revoke", "roles.manage", "credentials.update"] - ), - new( - "member", - "Member", - "Personal identity access", - 2, - false, - ["users.query", "sessions.query", - "sessions.revoke", "credentials.update"] - ) - ]; - - private RolePreview SelectedRole => - _roles.First(x => x.Id == _selectedRoleId); - - private string _selectedRoleId = "owner"; - - private IEnumerable FilteredPermissions => - _permissions.Where(x => - string.IsNullOrWhiteSpace(_permissionSearch) || - x.Name.Contains(_permissionSearch, StringComparison.OrdinalIgnoreCase) || - x.Description.Contains(_permissionSearch, StringComparison.OrdinalIgnoreCase)); + await dialog.Result; + + await LoadRolesAsync(); + } - private void SelectRole(RolePreview role) + private async Task EditPermissionsAsync() { - _selectedRoleId = role.Id; + if (SelectedRole is null) + return; + + var parameters = new DialogParameters + { + { x => x.Role, SelectedRole }, + { x => x.PreviewMode, false } + }; + + var dialog = await DialogService.ShowAsync( + "Edit Permissions", + parameters, + UAuthDialog.GetDialogOptions(MaxWidth.Large)); + + await dialog.Result; + + await LoadRolesAsync(); } - private sealed record PermissionPreview( - string Id, - string Name, - string Description); - - private sealed record RolePreview( - string Id, - string Name, - string Description, - int MemberCount, - bool IsOwner, - string[] PermissionIds); + private async Task EvaluatePolicyAsync() + { + if (_evaluating || + string.IsNullOrWhiteSpace(_playgroundAction) || + string.IsNullOrWhiteSpace(_playgroundResource)) + return; + + _evaluating = true; + _evaluationResult = null; + + try + { + var request = new AuthorizationCheckRequest + { + Action = _playgroundAction.Trim(), + Resource = _playgroundResource.Trim(), + ResourceId = string.IsNullOrWhiteSpace(_playgroundResourceId) + ? null + : _playgroundResourceId.Trim() + }; + + var result = await UAuthClient.Authorization.CheckAsync(request); + + if (!result.IsSuccess || result.Value is null) + { + Snackbar.Add( + result.ErrorText ?? "Authorization evaluation failed.", + Severity.Error); + + return; + } + + _evaluationResult = result.Value; + _evaluatedAction = request.Action; + _evaluatedResource = request.Resource; + } + catch (Exception) + { + Snackbar.Add( + "Unable to evaluate authorization.", + Severity.Error); + } + finally + { + _evaluating = false; + } + } } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor index 4a7af9ae..a89afc1a 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor @@ -1,9 +1,13 @@ @page "/dashboard/access" @layout DashboardLayout -@* TODO: Enable after authentication integration. @attribute [UAuthAuthorize] -*@ + +@using CodeBeam.UltimateAuth.Authorization.Contracts +@using CodeBeam.UltimateAuth.Core.Contracts + +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar My Access · Identity Lab @@ -35,26 +39,23 @@ Assigned Roles - 1 - Example role assignment + @_totalRoles + Roles assigned to your identity -
+ @*
Effective Permissions 4 Illustrative permission set -
+
*@
- - - - Access Context - Member - Example sandbox role + Available Actions + @_availablePermissions.Count + Discoverable authorization actions
@@ -71,21 +72,55 @@
- - - + @foreach (var role in _roles) + { +
-
- Member - Example sandbox membership role -
+ + + + +
+ @role.Name + + Assigned @role.AssignedAt.ToLocalTime().ToString("g") + +
+ + + Assigned + + +
+ } + + @if (!_loading && _totalRoles == 0 && _error is null) + { +
+ No roles assigned +

You do not currently have any assigned roles.

+
+ } + + @if (_error is not null) + { + + @_error + + } - - Assigned - + @if (_totalRoles > RolePageSize) + { + + }
-
+ @*

Effective Permissions

@@ -131,7 +166,7 @@
}
-
+
*@
@@ -156,38 +191,41 @@
- - Users - Sessions - Credentials - - Query - Create - Update - Revoke + @bind-Value="_selectedAction" + Searchable="true"> + + @foreach (var permission in _availablePermissions) + { + + @permission + + } + + @bind-Value="_selectedResource" /> + + - Evaluate Access + Disabled="@(_evaluating || string.IsNullOrWhiteSpace(_selectedAction))" + OnClick="EvaluateAccessAsync"> + + @(_evaluating ? "Evaluating..." : "Evaluate Access") +
@@ -203,16 +241,53 @@
- - - + @if (_accessResult is null) + { +
+ + + - Ready to evaluate -

- Live access decisions will appear here once - UltimateAuth authorization is connected. -

+ Ready to evaluate + +

+ Select a permission to check your current access. +

+
+ } + else + { + + + + @(_accessResult.IsAllowed + ? "Access Granted" + : "Access Denied") + + + @if (_accessResult.RequiresReauthentication) + { +

Reauthentication is required.

+ } + else if (!string.IsNullOrWhiteSpace(_accessResult.DenyReason)) + { +

@_accessResult.DenyReason

+ } + else + { +

+ @(_accessResult.IsAllowed + ? "Your current session is authorized." + : "Your current session is not authorized.") +

+ } + +
+ }
@code { + private readonly List _roles = []; + + private bool _loading; + private string? _error; + + private int _rolePage = 1; + private const int RolePageSize = 10; + private int _totalRoles; + + private int TotalRolePages => Math.Max(1, (_totalRoles + RolePageSize - 1) / RolePageSize); + + private string? _selectedAction; private string _selectedResource = "users"; - private string _selectedOperation = "query"; - - private readonly List _permissions = - [ - new("users.query.self", "View your own user information"), - new("sessions.query.self", "Inspect your own sessions"), - new("credentials.update.self", "Manage your own credentials"), - new("sessions.revoke.self", "Revoke your own sessions") - ]; - - private sealed record PermissionPreview( - string Name, - string Description); + private string? _selectedResourceId; + + private UltimateAuth.Authorization.Contracts.AuthorizationResult? _accessResult; + private bool _evaluating; + + protected override async Task OnInitializedAsync() + { + await LoadRolesAsync(); + } + + private async Task LoadRolesAsync() + { + _loading = true; + _error = null; + + try + { + var result = await UAuthClient.Authorization.GetMyRolesAsync( + new PageRequest + { + PageNumber = _rolePage, + PageSize = RolePageSize + }); + + if (!result.IsSuccess || result.Value is null) + { + _error = result.ErrorText ?? "Unable to load your roles."; + return; + } + + _roles.Clear(); + _roles.AddRange(result.Value.Roles.Items); + + _totalRoles = result.Value.Roles.TotalCount; + } + catch (Exception) + { + _error = "Unable to load your roles."; + } + finally + { + _loading = false; + } + } + + private async Task OnRolePageChangedAsync(int page) + { + _rolePage = page; + await LoadRolesAsync(); + } + + private readonly List _availablePermissions = + UAuthPermissionCatalog.GetAdminPermissions() + .OrderBy(x => x) + .ToList(); + + private async Task EvaluateAccessAsync() + { + if (_evaluating || + string.IsNullOrWhiteSpace(_selectedAction) || + string.IsNullOrWhiteSpace(_selectedResource)) + return; + + _evaluating = true; + _accessResult = null; + + try + { + var result = await UAuthClient.Authorization.CheckAsync( + new AuthorizationCheckRequest + { + Action = _selectedAction, + Resource = _selectedResource.Trim(), + ResourceId = string.IsNullOrWhiteSpace(_selectedResourceId) + ? null + : _selectedResourceId.Trim() + }); + + if (!result.IsSuccess || result.Value is null) + { + Snackbar.Add( + result.ErrorText ?? "Access evaluation failed.", + Severity.Error); + return; + } + + _accessResult = result.Value; + } + catch (Exception) + { + Snackbar.Add("Unable to evaluate access.", Severity.Error); + } + finally + { + _evaluating = false; + } + } } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor index 0d8c2f48..9ecfb863 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyIdentityPage.razor @@ -1,10 +1,11 @@ @page "/dashboard/identity" +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts @layout DashboardLayout +@inject IUAuthClient UAuthClient @inject IDialogService DialogService -@* TODO: Enable after authentication integration. @attribute [UAuthAuthorize] -*@ My Identity · Identity Lab @@ -17,14 +18,27 @@

My Identity

Explore your account, sign-in identifiers, and credential security.

- - - Demo Preview -
+ + @* + TODO (v0.2.x): Multi-profile management. + + UltimateAuth supports multiple profiles per user. + Identity Lab should demonstrate: + - Listing available profiles + - Switching between profiles + - Creating and deleting non-default profiles + - Editing the selected profile + + Prerequisite: + Add the necessary profile discovery/listing methods + to the UltimateAuth Users Client API. + + Keep the default profile as the initial implementation. + *@
@@ -41,42 +55,132 @@
- Sandbox User - @@demo-user + @_profile?.FirstName @_profile?.LastName + @@ @_profile?.UserName
- - Active + + @_profile?.Status
Display name - Sandbox User + @_profile?.DisplayName
Username - demo-user + @_profile?.UserName
Email - demo@example.com + @_profile?.PrimaryEmail +
+ +
+ Primary phone + @(_profile?.PrimaryPhone ?? "Not provided")
- Account status - Active + User Key + @_profile?.UserKey.Value
+ + + + Additional Details + + + +
+
+ Profile key + @_profile?.ProfileKey +
+ +
+ First name + @FormatValue(_profile?.FirstName) +
+ +
+ Last name + @FormatValue(_profile?.LastName) +
+ +
+ Birth date + @(_profile?.BirthDate?.ToString("d") ?? "Not provided") +
+ +
+ Gender + @FormatValue(_profile?.Gender) +
+ +
+ Bio + @FormatValue(_profile?.Bio) +
+ +
+ Language + @FormatValue(_profile?.Language) +
+ +
+ Culture + @FormatValue(_profile?.Culture) +
+ +
+ Time zone + @FormatValue(_profile?.TimeZone) +
+ +
+ Created at + + @(_profile?.CreatedAt?.ToLocalTime().ToString("g") ?? "—") + +
+ +
+ Metadata + + @if (_profile?.Metadata is { Count: > 0 } metadata) + { + + @foreach (var entry in metadata) + { + + @entry.Key: @entry.Value + + } + + } + else + { + No metadata + } +
+
+
+
- -
- - - - -
- demo@example.com - Email address -
- - - Verified - -
- -
- - - - -
- demo-user - Username -
- - - Primary - -
- + @if (_identifiersLoading) + { + + } + else if (_identifiersError is not null) + { + + @_identifiersError + + } + else if (_identifiers.Count == 0) + { + + No identifiers found. + + } + else + { + @foreach (var identifier in _identifiers) + { +
+ + + + +
+ @identifier.Value + @identifier.Type +
+ + @if (identifier.IsPrimary) + { + + Primary + + } + + + @(identifier.IsVerified ? "Verified" : "Not verified") + +
+ } + }
- Add Identifier + Manage Identifiers + + Email and phone built in verification workflows are coming soon. + +
@@ -223,66 +342,55 @@
-
-

Authentication Context

-

- Understand your UltimateAuth identity. -

+

Account Management

+

Manage your account status and lifecycle.

-
-
-
- - - +
+ + + -
- Identity - Your authenticated account -
+
+ Account Status +

Manage suspension, reactivation and account access.

-
- -
- - - - -
- Authentication Chain - Explore your authentication lifecycle -
-
+ + Manage + +
-
+ @if (_isSandboxOwner) + { + -
- - +
+ +
- Sessions - Manage active device sessions + Close Sandbox +

Close your sandbox and its associated resources.

-
-
- - - This is an illustrative overview. - Live authentication data will appear after integration. - + + Coming Soon + +
+ }
@@ -292,43 +400,175 @@ @code { - private async Task OpenEditProfileAsync() + [CascadingParameter] + private UAuthState AuthState { get; set; } = default!; + + private UserView? _profile; + private bool _loading = true; + private string? _loadError; + private bool _showDetails; + private readonly List _identifiers = []; + private bool _identifiersLoading = true; + private string? _identifiersError; + private bool _isSandboxOwner; + + private static string FormatValue(string? value) => string.IsNullOrWhiteSpace(value) ? "Not provided" : value; + + protected override async Task OnInitializedAsync() + { + await LoadProfileAsync(); + await LoadIdentifiersAsync(); + } + + private async Task LoadProfileAsync() { - var parameters = new DialogParameters + _loading = true; + _loadError = null; + + try { - { x => x.PreviewMode, true }, - { x => x.PreviewDisplayName, "Alex Morgan" } - }; + var result = await UAuthClient.Users.GetMeAsync(); + + if (result.IsSuccess) + _profile = result.Value; + else + _loadError = "Unable to load your profile."; + } + catch (Exception) + { + _loadError = "An error occurred while loading your profile."; + } + finally + { + _loading = false; + await InvokeAsync(StateHasChanged); + } + } + + private async Task LoadIdentifiersAsync() + { + _identifiersLoading = true; + _identifiersError = null; + + try + { + var collected = new List(); + var pageNumber = 1; + const int pageSize = 50; + + while (true) + { + var result = await UAuthClient.Identifiers.GetMyAsync( + new PageRequest + { + PageNumber = pageNumber, + PageSize = pageSize + }); + + if (!result.IsSuccess || result.Value is null) + { + _identifiersError = + result.ErrorText ?? "Unable to load identifiers."; + return; + } + + collected.AddRange(result.Value.Items); + + if (collected.Count >= result.Value.TotalCount || + result.Value.Items.Count() == 0) + break; + + pageNumber++; + } + + _identifiers.Clear(); + _identifiers.AddRange(collected); + } + catch (Exception) + { + _identifiersError = "Unable to load identifiers."; + } + finally + { + _identifiersLoading = false; + await InvokeAsync(StateHasChanged); + } + } - await DialogService.ShowAsync( + private async Task OpenEditProfileAsync() + { + var dialog = await DialogService.ShowAsync( "Edit Profile", - parameters, + new DialogParameters + { + { x => x.PreviewMode, false } + }, UAuthDialog.GetDialogOptions(MaxWidth.Medium)); + + var result = await dialog.Result; + + if (result is { Canceled: false }) + await LoadProfileAsync(); } private async Task OpenIdentifiersAsync() { - var parameters = new DialogParameters - { - { x => x.PreviewMode, true } - }; - - await DialogService.ShowAsync( + var dialog = await DialogService.ShowAsync( "Manage Identifiers", - parameters, + new DialogParameters + { + { x => x.PreviewMode, false }, + { x => x.EnableVerification, false } + }, UAuthDialog.GetDialogOptions(MaxWidth.Medium)); + + var result = await dialog.Result; + + if (result is { Canceled: false }) + { + await LoadIdentifiersAsync(); + await LoadProfileAsync(); + } } private async Task OpenChangePasswordAsync() { - var parameters = new DialogParameters - { - { x => x.PreviewMode, true } - }; - await DialogService.ShowAsync( "Change Password", - parameters, + new DialogParameters + { + { x => x.PreviewMode, false } + }, UAuthDialog.GetDialogOptions(MaxWidth.Small)); } + + private async Task OpenAccountManagementAsync() + { + var dialog = await DialogService.ShowAsync( + "Account Management", + new DialogParameters + { + { x => x.PreviewMode, false }, + { x => x.AuthState, AuthState }, + { x => x.AllowAccountDeletion, !_isSandboxOwner } + }, + UAuthDialog.GetDialogOptions(MaxWidth.Medium)); + + var result = await dialog.Result; + + if (result is { Canceled: false }) + { + await LoadIdentifiersAsync(); + await LoadProfileAsync(); + } + } + + private static string GetIdentifierIcon(UserIdentifierType type) + => type switch + { + UserIdentifierType.Email => Icons.Material.Filled.Email, + UserIdentifierType.Phone => Icons.Material.Filled.Phone, + UserIdentifierType.Username => Icons.Material.Filled.Badge, + _ => Icons.Material.Filled.AlternateEmail + }; } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor index fd77d9db..e474690c 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor @@ -1,386 +1,544 @@ @page "/dashboard/sessions" @layout DashboardLayout -@inject IDialogService DialogService - -@* TODO: Enable after authentication integration. @attribute [UAuthAuthorize] -*@ +@using CodeBeam.UltimateAuth.Client +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject NavigationManager Navigation My Sessions · Identity Lab
-
PERSONAL SECURITY

My Sessions

-

- Explore your devices, authentication sessions, - and the UltimateAuth session lifecycle. -

+

Explore your devices, authentication sessions, and the UltimateAuth session lifecycle.

- - - Demo Preview - + Refresh
+ @if (_loading) + { + + } + @if (_error is not null) + { + @_error + } +
- - - - Active sessions - 2 - Example session count + + Active devices + @(_chains.Count(x => x.State == SessionChainState.Active)) + On this page
-
- - - + Authentication chains - 2 - Illustrative chains + @_totalChains + Total chain records
-
- - - - Security status - Active - Example state + + Current device + @(_currentChain is not null ? "Identified" : "Not located") + Server-reported session context
-

Active Devices

-

- Sessions associated with your identity. -

+

Devices & Chains

+

Session chains associated with your account.

- - - Revoke Other Sessions - + Disabled="@(_busy || _totalChains < 2)" + OnClick="LogoutOthersAsync">Logout Other Sessions
-
- -
- - - - -
-
- Windows · Chrome - - - Current Device - -
+ + @* TODO (SDK): Provide direct current-chain lookup without scanning pages. *@ + @if (_currentChain is not null) + { +
+
+ + + -

Example desktop session

- -
- - - Active now (example) - - - - Password authentication - -
-
+
+
+ @DeviceName(_currentChain) - - Details - -
+ + Current Device + +
-
- - - +

@(_currentChain.Platform ?? "Platform unavailable")

-
-
- Mobile · Safari +
+ + + Last seen: + @(_currentChain.LastSeenAt?.ToLocalTime().ToString("g") ?? "Unknown") + - - Remote Session - + + + Chain: @_currentChain.ChainId + +
-

Example mobile session

- -
- - - Recently active (example) - - - - Password authentication - -
-
+ + Details + + + @if (!_currentChain.IsRevoked) + { + + Logout + + } +
+
+ } + else if (!_loading && _error is null) + { + + Locate Current Device + + } - - Revoke - - +
+ @if (!_loading && _error is null && _chains.Count == 0) + { + No session chains were returned. + } + @foreach (var chain in _chains.Where(x => !x.IsCurrentDevice)) + { +
+ + + +
+
+ @DeviceName(chain) + @if (chain.IsCurrentDevice) + { + Current Device + } + else + { + @chain.State + } +
+

@(chain.Platform ?? "Platform unavailable")

+
+ + + Last seen: @(chain.LastSeenAt?.ToLocalTime().ToString("g") ?? "Unknown") + + + + Chain: @chain.ChainId + +
+
+ Details + @if (!chain.IsRevoked) + { + Logout + } +
+ } +
+ +
+ + + Page @_page of @TotalPages · @_totalChains chains +
ULTIMATEAUTH ARCHITECTURE

Authentication Hierarchy

-

- Understand the relationship between authentication - roots, chains, and sessions. -

+

Root → Chain → Session. Open Details to inspect actual chain and session history.

-
-
-
-

Session Structure

- +

My Chains

+
-
-
- - - - -
- Authentication Root - Example authentication origin -
- - - Active - + +
Authentication RootCurrent account
-
- -
- - - - -
- Chain A - Example desktop chain + @if (_currentChain is not null) + { +
+ +
@DeviceName(_currentChain)Current · @_currentChain.State
+ Sessions
-
- -
- - -
- Session A1 - Windows · Chrome + } + @foreach (var chain in _chains.Where(x => !x.IsCurrentDevice).Take(_treeVisibleCount)) + { +
+ +
@DeviceName(chain)@chain.State · @chain.ChainId
+ Sessions
- - - Current - -
- -
- - - - -
- Chain B - Example mobile chain -
-
- -
- - -
- Session B1 - Mobile · Safari -
-
- + } + @if (_chains.Count(x => !x.IsCurrentDevice) > _treeVisibleCount) + { + Show more chains + } + @if (_totalChains > PageSize) + { + Showing chains from page @_page of @TotalPages. Use pagination above to browse other chains. + }
-
-
-

How It Works

-
-
01 -
- Root -

- Represents the origin of an authentication - relationship. -

-
+
Root

Account-level session authority.

-
02 -
- Chain -

- Groups related authentication activity - within a lifecycle. -

-
+
Chain

Device-associated authentication continuity.

-
03 -
- Session -

- Represents a session that can be inspected - and managed according to its security context. -

-
+
Session

An authentication instance within a chain.

-
- - - This hierarchy is illustrative. The actual structure - depends on the authentication mode and session lifecycle. + + Device information is descriptive and does not prove physical device identity. -
-
-
-

Session Security

-

- Manage your authentication lifecycle and device access. -

-
+

Session Security

Manage your authentication lifecycle and device access.

-
-
- - - +
- Revoke other sessions -

- End other device sessions while keeping - your current session. -

+ Logout other sessions +

Logout other chains while keeping your current chain.

- - Revoke Others - + Logout Others
- +
- + +
- Sign out -

- End your current authenticated session. -

+ Logout all sessions +

Logout every device chain, including your current session.

+ - Sign Out + Disabled="@_busy" + OnClick="LogoutAllAsync"> + Logout All
+
+ +
Sign out

End your current authenticated session.

+ Sign Out +
- @code { - private async Task OpenSessionManagementAsync() + private readonly List _chains = []; + private bool _loading = true; + private bool _busy; + private string? _error; + private int _page = 1; + private int _totalChains; + private SessionChainSummary? _currentChain; + private int _treeVisibleCount = 4; + private int TotalPages => Math.Max(1, (_totalChains + PageSize - 1) / PageSize); + private const int PageSize = 20; + + protected override async Task OnInitializedAsync() => await LoadAsync(); + + private async Task RefreshAsync() + { + _page = 1; + await LoadAsync(); + } + + private async Task OnPageChangedAsync(int page) + { + _page = page; + _treeVisibleCount = 4; + await LoadAsync(); + } + + private void ExpandTree() => _treeVisibleCount += 5; + + // Fallback without a current-chain endpoint. Run only on explicit user request: + // a full scan on every page load would not scale for accounts with many chains. + // TODO (SDK): Replace with GetMyCurrentChainAsync when available. + private async Task FindCurrentChainAsync() + { + if (_busy) return; + _busy = true; + try + { + for (var page = 1; page <= TotalPages; page++) + { + var result = await UAuthClient.Sessions.GetMyChainsAsync(CreateChainsRequest(_page)); + + if (!result.IsSuccess || result.Value is null) + { + Snackbar.Add(result.ErrorText ?? "Unable to locate current device.", Severity.Error); + return; + } + _totalChains = result.Value.TotalCount; + var current = result.Value.Items.FirstOrDefault(x => x.IsCurrentDevice); + if (current is not null) + { + _currentChain = current; + return; + } + } + Snackbar.Add("Current device was not found in the available chains.", Severity.Info); + } + catch (Exception) + { + Snackbar.Add("Unable to locate current device.", Severity.Error); + } + finally { _busy = false; } + } + + private async Task LoadAsync() + { + if (_busy) return; + _busy = true; + _loading = true; + _error = null; + try + { + var result = await UAuthClient.Sessions.GetMyChainsAsync(CreateChainsRequest(_page)); + + if (!result.IsSuccess || result.Value is null) + { + _error = result.ErrorText ?? "Unable to load session chains."; + return; + } + _chains.Clear(); + _chains.AddRange(result.Value.Items); + _totalChains = result.Value.TotalCount; + _currentChain = _chains.FirstOrDefault(x => x.IsCurrentDevice) ?? _currentChain; + } + catch (Exception) + { + _error = "Session service is unavailable."; + } + finally + { + _loading = false; + _busy = false; + } + } + + private async Task OpenSessionManagementAsync(SessionChainId chainId) { var parameters = new DialogParameters { - { x => x.PreviewMode, true } + { x => x.PreviewMode, false }, + { x => x.InitialChainId, chainId } }; - await DialogService.ShowAsync( - "Session Management", + var dialog = await DialogService.ShowAsync( + "Session Details", parameters, UAuthDialog.GetDialogOptions(MaxWidth.Large)); + + await dialog.Result; + await LoadAsync(); + } + + private async Task ConfirmAsync(string title, string message, string action) + => await DialogService.ShowMessageBoxAsync(title, message, yesText: action, noText: "Cancel") == true; + + private async Task LogoutAsync(SessionChainSummary chain) + { + if (_busy || !await ConfirmAsync("Logout device", + chain.IsCurrentDevice + ? "This is your current device. Logout it may sign you out. Continue?" + : "Logout this device chain?", "Logout")) return; + _busy = true; + try + { + var result = await UAuthClient.Flows.LogoutMyDeviceAsync(new LogoutDeviceRequest() { ChainId = chain.ChainId }); + if (!result.IsSuccess) + { + Snackbar.Add(result.ErrorText ?? "Unable to logout device.", Severity.Error); + return; + } + + Snackbar.Add("Logout from device chain.", Severity.Success); + if (result.Value?.CurrentChain == true) + { + Navigation.NavigateTo("/login"); + return; + } + } + catch (Exception) { Snackbar.Add("Unable to logout device.", Severity.Error); } + finally { _busy = false; } + await LoadAsync(); + } + + private async Task LogoutOthersAsync() + { + if (_busy || !await ConfirmAsync("Logout other sessions", + "Logout all other device chains and keep the current chain?", "Logout Others")) return; + _busy = true; + try + { + var result = await UAuthClient.Flows.LogoutMyOtherDevicesAsync(); + Snackbar.Add(result.IsSuccess ? "Logout from other chains." : + result.ErrorText ?? "Unable to logout other chains.", + result.IsSuccess ? Severity.Success : Severity.Error); + } + catch (Exception) { Snackbar.Add("Unable to logout other chains.", Severity.Error); } + finally { _busy = false; } + + await LoadAsync(); } + + private async Task LogoutAllAsync() + { + if (_busy || !await ConfirmAsync( + "Logout all sessions", + "This will end all your device sessions, including the current one. Continue?", + "Logout All")) + return; + + _busy = true; + + try + { + var result = await UAuthClient.Flows.LogoutAllMyDevicesAsync(); + + if (!result.IsSuccess) + { + Snackbar.Add(result.ErrorText ?? "Unable to logout all sessions.", Severity.Error); + return; + } + + Navigation.NavigateTo("/login", forceLoad: true); + } + catch (Exception) + { + Snackbar.Add("Unable to logout all sessions.", Severity.Error); + } + finally + { + _busy = false; + } + } + + private async Task SignOutAsync() + { + if (_busy || !await ConfirmAsync("Sign out", "End your current session?", "Sign Out")) return; + _busy = true; + try + { + await UAuthClient.Flows.LogoutAsync(); + Navigation.NavigateTo("/login"); + } + catch (Exception) + { + Snackbar.Add("Unable to sign out.", Severity.Error); + _busy = false; + } + } + + private static string DeviceName(SessionChainSummary chain) + => $"{chain.OperatingSystem ?? "Unknown OS"} · {chain.Browser ?? "Unknown browser"}"; + + private static string DeviceIcon(string? deviceType) + => deviceType?.Contains("mobile", StringComparison.OrdinalIgnoreCase) == true + ? Icons.Material.Filled.Smartphone + : Icons.Material.Filled.Computer; + + private PageRequest CreateChainsRequest(int page) => new() + { + PageNumber = page, + PageSize = PageSize, + SortBy = nameof(SessionChainSummary.CreatedAt), + Descending = true + }; } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor.css b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor.css index dd58731e..440fb231 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor.css +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MySessionsPage.razor.css @@ -332,6 +332,31 @@ line-height: 1.5; } + +.lab-device-main { + flex: 1 1 0; + min-width: 0; +} + +.lab-device-title { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.lab-device-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 20px; +} + + .lab-device-meta span { + min-width: 0; + overflow-wrap: anywhere; + } + + @media (max-width: 1100px) { .lab-hierarchy-layout { grid-template-columns: 1fr; diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/SettingsPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/SettingsPage.razor index 78af858c..860ed21d 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/SettingsPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/SettingsPage.razor @@ -1,9 +1,15 @@ @page "/dashboard/settings" +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Services @layout DashboardLayout -@* TODO: Enable sandbox-owner authorization. @attribute [UAuthAuthorize] -*@ +@inherits UAuthComponentBase + +@inject SandboxService SandboxService +@inject SandboxUserService SandboxUserService +@inject ISnackbar Snackbar Sandbox Settings · Identity Lab @@ -19,14 +25,6 @@ membership limits, and security boundaries.

- - - Demo Preview -
@@ -44,31 +42,47 @@
+ + ReadOnly="@(!_isOwner)" + MaxLength="150" + Immediate="true" /> + HelperText="Unique identifier assigned to your sandbox." /> - - Save Changes - + @if (_isOwner) + { + + + @if (_saving) + { + + } + + Save Changes + + } +
@@ -83,31 +97,36 @@
+
Occupied user slots - 3 / 4 + + + @_memberCount / @_maxUsers +
-

- Your sandbox supports up to four accounts, + Your sandbox supports up to @_maxUsers accounts, including the owner.

+
Maximum Users - 4 + @_maxUsers
Available Slots - 1 + @_availableSlots
@@ -146,14 +165,9 @@
Private Sandbox -

- Users and resources are scoped to - their own sandbox. -

+

Sandbox membership determines which users and resources belong to this environment.

- +
@@ -162,10 +176,7 @@
Scoped Administration -

- Owners manage only identities and - resources within their sandbox. -

+

Sandbox owners can manage their sandbox through application-level access controls.

Membership Limit -

- A maximum of four accounts prevents - unrestricted sandbox growth. -

+

Each sandbox supports up to @_maxUsers accounts, including the owner.

- These are planned Identity Lab security rules. - They must be enforced server-side before launch. + Identity Lab applies application-level sandbox access controls. + Sandbox isolation must also be enforced by every operation + that accesses or modifies shared UltimateAuth resources. @@ -283,3 +292,142 @@
+ +@code { + private Sandbox? _sandbox; + + private UserKey _currentUserKey; + + private string _sandboxName = string.Empty; + private string _ownerName = string.Empty; + + private int _memberCount; + private int _maxUsers; + + private bool _isOwner; + private bool _saving; + private bool _loading = true; + + private bool _hasChanges => _sandbox is not null && !string.Equals(_sandbox.Name, _sandboxName?.Trim(), StringComparison.Ordinal); + + private int _availableSlots => Math.Max(0, _maxUsers - _memberCount); + + private double _capacityPercentage => _maxUsers > 0 ? Math.Min(100d, _memberCount * 100d / _maxUsers) : 0; + + private string? _loadedUserKey; + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + + if (AuthState?.Identity?.UserKey is not UserKey userKey) + { + _sandbox = null; + _loadedUserKey = null; + _isOwner = false; + _loading = false; + return; + } + + if (_loadedUserKey == userKey.Value) + return; + + await LoadSettingsAsync(userKey); + + if (_sandbox is not null) + _loadedUserKey = userKey.Value; + } + + private async Task SaveChangesAsync() + { + if (!_isOwner || _sandbox is null || _saving) + return; + + var name = _sandboxName.Trim(); + + if (string.IsNullOrWhiteSpace(name)) + { + Snackbar.Add("Sandbox name is required.", Severity.Warning); + return; + } + + if (name.Length > 150) + { + Snackbar.Add("Sandbox name cannot exceed 150 characters.", Severity.Warning); + return; + } + + _saving = true; + + try + { + var updated = await SandboxService.RenameAsync( + _currentUserKey, + name); + + if (!updated) + { + Snackbar.Add("Unable to update sandbox.", Severity.Error); + return; + } + + _sandbox.Name = name; + _sandboxName = name; + + Snackbar.Add("Sandbox settings updated.", Severity.Success); + } + catch (Exception) + { + Snackbar.Add("Unable to save sandbox settings.", Severity.Error); + } + finally + { + _saving = false; + } + } + + private async Task LoadSettingsAsync(UserKey currentUserKey) + { + _loading = true; + + try + { + _currentUserKey = currentUserKey; + + _sandbox = await SandboxService.GetByUserAsync(currentUserKey); + + if (_sandbox is null) + { + Snackbar.Add("Sandbox not found.", Severity.Warning); + return; + } + + _sandboxName = _sandbox.Name; + + _isOwner = _sandbox.OwnerUserKey == currentUserKey; + + var members = await SandboxService.GetMembersAsync(_sandbox.Id); + + _memberCount = members.Count; + + _maxUsers = Math.Min( + _sandbox.Settings.MaxUsers, + SandboxLimits.AbsoluteMaxUsers); + + var snapshot = await SandboxUserService.GetUsersAsync(currentUserKey); + + var owner = snapshot?.Users.FirstOrDefault(x => x.IsOwner); + + _ownerName = owner?.DisplayName + ?? _sandbox.OwnerUserKey.Value; + } + catch (Exception) + { + Snackbar.Add("Unable to load sandbox settings.", Severity.Error); + } + finally + { + _loading = false; + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/UsersPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/UsersPage.razor index 79bbd06c..6af62350 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/UsersPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/UsersPage.razor @@ -1,10 +1,14 @@ @page "/dashboard/users" +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Contracts +@using CodeBeam.UltimateAuth.IdentityLab.Services +@using CodeBeam.UltimateAuth.Users.Contracts @layout DashboardLayout +@inject SandboxUserService SandboxUserService @inject IDialogService DialogService -@* TODO: Enable sandbox-scoped authorization. -@attribute [UAuthAuthorize] -*@ +@attribute [UAuthAuthorize(Roles = "Ladmin,Owner")] Users · Identity Lab @@ -15,16 +19,10 @@
SANDBOX MANAGEMENT

Users

-

- Manage identities, account status, and - membership within your sandbox. -

+

Manage identities, account status, and membership within your sandbox.

- + Create User @@ -37,25 +35,11 @@ Sandbox Capacity @_users.Count / @MaxUsers users - - - Demo Preview - - + -

- @_users.Count of @MaxUsers available user slots are - occupied, including the sandbox owner. -

+

@_users.Count of @MaxUsers available user slots are occupied, including the sandbox owner.

@@ -64,7 +48,7 @@ Total Users @_users.Count - Example sandbox members + Sandbox members
@@ -73,7 +57,7 @@ Active Users @_users.Count(x => x.Status == UserStatus.Active) - Example active accounts + Active accounts
@@ -119,12 +103,20 @@
- + @_error + + } + + @@ -138,8 +130,7 @@
- + @context.Initials @@ -179,29 +170,17 @@ @@ -252,40 +231,116 @@
@code { - private const int MaxUsers = 4; + [CascadingParameter] + private UAuthState? AuthState { get; set; } + + private SandboxUsersSnapshot? _snapshot; + private List _users = []; + + private UserKey? _loadedUserKey; + private bool _loading; + private string? _error; + + private int MaxUsers => _snapshot?.MaxUsers ?? 0; + + private double CapacityPercentage => _snapshot?.CapacityPercentage ?? 0; + + protected override async Task OnParametersSetAsync() + { + var userKey = AuthState?.Identity?.UserKey; + + if (userKey is null) + { + _loadedUserKey = null; + _snapshot = null; + _users.Clear(); + return; + } + + if (_loadedUserKey == userKey) + return; + + await LoadUsersAsync(userKey.Value); + } + + private async Task LoadUsersAsync(UserKey userKey) + { + _loading = true; + _error = null; + + try + { + var snapshot = await SandboxUserService.GetUsersAsync( + userKey); + + _snapshot = snapshot; + _users = snapshot?.Users.ToList() ?? []; + + _loadedUserKey = userKey; + } + catch (Exception) + { + _error = "Unable to load sandbox users."; + _snapshot = null; + _users.Clear(); + } + finally + { + _loading = false; + } + } private string _search = string.Empty; private string _statusFilter = "all"; - private double CapacityPercentage => (double)_users.Count / MaxUsers * 100; - private async Task OpenCreateUserDialogAsync() { var parameters = new DialogParameters { - { x => x.PreviewMode, true } + { x => x.PreviewMode, false }, + { x => x.OnCreateUserAsync, CreateSandboxUserAsync } }; - await DialogService.ShowAsync("Create User", parameters, UAuthDialog.GetDialogOptions(MaxWidth.Small)); + var dialog = await DialogService.ShowAsync( + "Create User", + parameters, + UAuthDialog.GetDialogOptions(MaxWidth.Small)); + + var result = await dialog.Result; + + if (result?.Canceled == false) + { + var userKey = AuthState?.Identity?.UserKey; + + if (userKey is not null) + await LoadUsersAsync(userKey.Value); + } } - private async Task OpenUserProfileAsync(string displayName) + private async Task OpenUserDetailsAsync(SandboxUserItem user) { - var parameters = new DialogParameters + if (user.IsOwner) + return; + + var parameters = new DialogParameters { - { x => x.PreviewMode, true }, - { x => x.PreviewDisplayName, displayName } + { x => x.AuthState, AuthState }, + { x => x.UserKey, user.UserKey }, + { x => x.PreviewMode, false } }; - await DialogService.ShowAsync( - "Edit Profile", - parameters, - UAuthDialog.GetDialogOptions(MaxWidth.Medium)); - } + var dialog = await DialogService.ShowAsync("Manage User", parameters, UAuthDialog.GetDialogOptions(MaxWidth.Large)); + + await dialog.Result; + var actorUserKey = AuthState?.Identity?.UserKey; - private IEnumerable FilteredUsers => + if (actorUserKey is not null) + await LoadUsersAsync(actorUserKey.Value); + } + + private IEnumerable FilteredUsers => _users.Where(user => { var matchesSearch = @@ -304,52 +359,13 @@ return matchesSearch && matchesStatus; }); - private readonly List _users = - [ - new( - "Alex Morgan", - "alex", - "alex@example.com", - true, - UserStatus.Active, - new DateTime(2026, 10, 18) - ), - new( - "Taylor Reed", - "taylor", - "taylor@example.com", - false, - UserStatus.Active, - new DateTime(2026, 10, 19) - ), - new( - "Jordan Lee", - "jordan", - "jordan@example.com", - false, - UserStatus.Disabled, - new DateTime(2026, 10, 20) - ) - ]; - - private enum UserStatus + private async Task CreateSandboxUserAsync(CreateUserRequest request) { - Active, - Disabled - } + var actorUserKey = AuthState?.Identity?.UserKey; - private sealed record SandboxUserPreview( - string DisplayName, - string Username, - string Email, - bool IsOwner, - UserStatus Status, - DateTime CreatedAt) - { - public string Initials => - string.Concat( - DisplayName.Split(' ', StringSplitOptions.RemoveEmptyEntries) - .Take(2) - .Select(x => char.ToUpperInvariant(x[0]))); + if (actorUserKey is null) + return CreateUserDialogResult.Failure("You must be signed in."); + + return await SandboxUserService.CreateAsync(actorUserKey.Value, request); } } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/DashboardPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/DashboardPage.razor index 804a657d..fff8c308 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/DashboardPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/DashboardPage.razor @@ -1,11 +1,17 @@ @page "/dashboard" @layout DashboardLayout -@* TODO: Enable after authentication integration. -@attribute [UAuthAuthorize] -*@ +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Authorization +@using CodeBeam.UltimateAuth.IdentityLab.Contracts +@using CodeBeam.UltimateAuth.IdentityLab.Services -Dashboard · Identity Lab +@attribute [UAuthAuthorize(Roles = "Ladmin,Owner,User")] + +@inject LabDashboardService DashboardService +@inject ISnackbar Snackbar + +Overview · Identity Lab
@@ -14,262 +20,434 @@
IDENTITY WORKSPACE

Welcome to your sandbox.

-

- Explore your identity, manage sessions, and - experience UltimateAuth in action. -

+

Review your identity, explore authentication and manage your sandbox with UltimateAuth.

- - - Demo Preview -
-
-
-

My Identity

-

Your personal authentication and security overview.

+ @if (_loading) + { + + } + else if (_dashboard is null) + { + + Your sandbox overview is currently unavailable. + + } + else + { + +
+
+

My Lab

+

+ A quick look at your authentication + and security context. +

+
-
-
+
-
- - - - Identity status - Authenticated - Example identity state -
+
+ + + -
- - - - My sessions - 2 - Example active sessions -
+ Identity Status -
- - - - My roles - 1 - Example assigned role -
+ + @(IsAuthenticated ? "Authenticated" : "Unauthenticated") + -
- - - - My permissions - View - Explore access decisions -
+ UltimateAuth authentication state +
-
+
+ + + -
+ Authentication Chains -
-
-
-

Identity Details

-

Your account and authentication context.

-
- + + @(_dashboard.ChainCount?.ToString() ?? "—") + + + Total authentication chains
-
- - - +
+ + + -
- Sandbox User - Example account -
+ My Roles - + + @(_dashboard.RoleCount?.ToString() ?? "—") + - - Active - + Assigned authorization roles
-
- Username - demo-user -
+
+ + + -
- Authentication - Password -
+ My Permissions -
- Session model - Root / Chain / Session + Explore + + Evaluate your access
- - Explore My Identity - -
- -
-
-
-

Session Management

-

Discover how device sessions are managed.

+
+ + + +
+ +
+ +
+
+

Identity Details

+

+ Your current authenticated identity. +

+
+ +
- -
-
- - - -
- Windows · Chrome - Example current device +
+ + + + + +
+ + @(!string.IsNullOrWhiteSpace(_dashboard.DisplayName) + ? _dashboard.DisplayName + : "My Identity") + + + + @(_dashboard.Email ?? "Authenticated account") + +
+ + + + + @(_dashboard.AccountStatus ?? "Authenticated") + +
- - - Current - -
-
- - - +
+ Username + @(_dashboard.Username ?? "—") +
+ +
+ Email + @(_dashboard.Email ?? "—") +
+ +
+ Sandbox Membership + + @(_dashboard.IsOwner ? "Owner" : "Member") + +
+ +
+ Username + @(_dashboard.Username ?? "—") +
+ +
+ Email + @(_dashboard.Email ?? "—") +
+ +
+ Sandbox Membership + @(_dashboard.IsOwner ? "Owner" : "Member") +
+ + + Explore My Identity + + +
+ +
+ +
+
+

Current Session

+

+ Your active authentication context. +

+
+ + +
+ +
+ + + + + +
+ Current Authentication + + Your current UltimateAuth session context + +
+ + + + + Active + + +
+ +
+ Authentication + + @(IsAuthenticated ? "Active" : "Unavailable") + +
+ +
+ Session Management + Available +
+ + + Manage My Sessions + + +
+ +
+ + + @if (ShowManagement) + { +
- Mobile · Safari - Example remote device + + SANDBOX MANAGEMENT + + +

Manage your environment

+ +

+ Manage identities, explore authorization, + and review activity within your sandbox. +

- -
- - Manage My Sessions - - +
-
+
- @if (ShowManagement) - { -
-
- SANDBOX MANAGEMENT -

Manage your environment

-

- Create users, configure authorization, and - experiment with identity operations. -

-
-
+ + + -
+ + + + + + + + + Authorization + +

+ Explore roles, permissions, + and access control. +

+ + + Explore authorization + + + + +
+ + + + + + + + Activity +

+ Review recent authentication + and sandbox activity. +

+ + + View activity + + + + +
+ + + + + + + + Settings + +

+ Review your sandbox configuration, + membership, and capacity. +

+ + + Sandbox settings + + + + +
+ +
+ } + +
- Your sandbox, your experiments. + @_dashboard.SandboxName +

- Each sandbox supports up to four users, - including its owner. + Your isolated Identity Lab environment. + Manage up to @_dashboard.MaxUsers accounts, including the owner.

- - 1 / 4 example users + + @_dashboard.MemberCount / @_dashboard.MaxUsers users
} -
@code { - // Temporary design preview. Must be replaced by - // server-validated, sandbox-scoped management permission. - private bool ShowManagement => true; + + [CascadingParameter] + private UAuthState? AuthState { get; set; } + + private LabDashboardSnapshot? _dashboard; + + private UserKey? _loadedUserKey; + + private bool _loading = true; + + private bool IsAuthenticated => AuthState?.Identity?.UserKey is not null; + + private string CurrentUserIdentifier => AuthState?.Identity?.UserKey.Value ?? "Unavailable"; + + private bool ShowManagement => + AuthState?.IsInRole(LabRoles.Admin) == true || + (AuthState?.IsInRole(LabRoles.Owner) == true && + _dashboard?.IsOwner == true); + + protected override async Task OnParametersSetAsync() + { + var userKey = AuthState?.Identity?.UserKey; + + if (userKey is null) + { + _dashboard = null; + _loadedUserKey = null; + _loading = false; + return; + } + + if (_loadedUserKey == userKey) + return; + + _loading = true; + + try + { + _dashboard = await DashboardService.GetAsync(userKey.Value); + + if (_dashboard is not null) + _loadedUserKey = userKey; + } + catch (Exception) + { + _dashboard = null; + + Snackbar.Add( + "Unable to load your dashboard.", + Severity.Error); + } + finally + { + _loading = false; + } + } } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dialogs/ManageSandboxDialog.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dialogs/ManageSandboxDialog.razor new file mode 100644 index 00000000..791f9236 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dialogs/ManageSandboxDialog.razor @@ -0,0 +1,258 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Contracts +@using CodeBeam.UltimateAuth.IdentityLab.Domain +@using CodeBeam.UltimateAuth.IdentityLab.Services + +@inject LabAdminService AdminService +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + + + + + + Manage Sandbox + + + + Update sandbox settings and inspect members. + + + + + + + + + + + + + + + + + + Active + + + + Passive + + + + Suspended + + + + + @if (_status != SandboxStatus.Active) + { + + Changing the sandbox status may restrict + member access. User account statuses will + not be changed. + + } + + + + + + + + + + User + Role + Status + Actions + + + + + + @context.DisplayName + + + @context.Email + + + + + @context.Role + + + + + @context.Status + + + + + + + + + + + + + + + + + + + @if (!string.IsNullOrWhiteSpace(_error)) + { + + @_error + + } + + + + + + + Cancel + + + + Save Changes + + + + +@code { + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [CascadingParameter] + private UAuthState AuthState { get; set; } = default!; + + [Parameter, EditorRequired] + public AdminSandboxItem Sandbox { get; set; } = default!; + + [Parameter, EditorRequired] + public UserKey ActorUserKey { get; set; } + + private string _name = string.Empty; + private int _maxUsers; + private SandboxStatus _status; + + private List _members = []; + + private bool _saving; + private string? _error; + + protected override async Task OnInitializedAsync() + { + _name = Sandbox.Name; + _maxUsers = Sandbox.MaxUsers; + _status = Sandbox.Status; + + await LoadMembersAsync(); + } + + private async Task LoadMembersAsync() + { + try + { + _members = (await AdminService.GetMemberDetailsAsync(ActorUserKey, Sandbox.SandboxId)).ToList(); + } + catch (Exception) + { + _error = "Unable to load sandbox members."; + } + + } + + private async Task ManageMemberAsync(AdminSandboxMemberItem item) + { + var parameters = new DialogParameters + { + { x => x.AuthState, AuthState }, + { x => x.UserKey, item.UserKey }, + { x => x.PreviewMode, false } + }; + + var dialog = await DialogService.ShowAsync("Manage User", parameters, UAuthDialog.GetDialogOptions(MaxWidth.Large)); + + await dialog.Result; + + var actorUserKey = AuthState?.Identity?.UserKey; + + if (actorUserKey is not null) + await LoadMembersAsync(); + } + + private async Task SaveAsync() + { + if (_saving) + return; + + _saving = true; + _error = null; + + try + { + await AdminService.UpdateSandboxAsync( + ActorUserKey, + Sandbox.SandboxId, + new UpdateSandboxRequest( + _name, + _maxUsers, + _status)); + + Snackbar.Add( + "Sandbox updated successfully.", + Severity.Success); + + MudDialog.Close(DialogResult.Ok(true)); + } + catch (Exception ex) + { + _error = ex.Message; + } + finally + { + _saving = false; + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor index dcc66f72..3c077146 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor @@ -1,6 +1,8 @@ @page "/login" @using CodeBeam.UltimateAuth.Core.Contracts @layout PublicLayout +@inject IUAuthClient UAuthClient +@inject NavigationManager NavigationManager Sign In · Identity Lab @@ -17,55 +19,116 @@ YOUR IDENTITY WORKSPACE -

Welcome back.

-

Sign in to continue to your Identity Lab sandbox.

+

+ @(AuthState.IsAuthenticated ? "You're signed in." : "Welcome back.") +

+ +

+ @(AuthState.IsAuthenticated + ? "Continue with your current account or sign out to switch accounts." + : "Sign in to continue to your Identity Lab sandbox.") +

@@ -141,20 +203,54 @@ @code { - private MudForm? _form; + [CascadingParameter] + private UAuthState AuthState { get; set; } = default!; + private string _identifier = string.Empty; private string _password = string.Empty; - private bool _rememberMe; - private bool _showPassword; + private string? _loginError; + private bool _signingOut; + + private string CurrentDisplayName => + AuthState.Identity?.DisplayName + ?? AuthState.Identity?.PrimaryUserName + ?? "User"; + + private string? CurrentUserName => + AuthState.Identity?.PrimaryUserName; - private void TogglePassword() + private void ContinueToDashboard() { - _showPassword = !_showPassword; + NavigationManager.NavigateTo("/dashboard"); + } + + private async Task SignOutAsync() + { + if (_signingOut) + return; + + _signingOut = true; + + try + { + await UAuthClient.Flows.LogoutAsync(); + } + finally + { + _signingOut = false; + } } private void HandleLoginResult(IUAuthTryResult result) { - // Sonraki adımda başarısız giriş mesajlarını - // ve hesap kilidi durumunu burada işleyeceğiz. + if (result.IsSuccess) + { + _loginError = null; + return; + } + + _loginError = result.LockoutUntilUtc is not null + ? "Your account is temporarily locked. Please try again later." + : "Sign in failed. Please check your credentials."; } } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/RegisterPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/RegisterPage.razor index c9aa085a..8d7fa3e4 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/RegisterPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/RegisterPage.razor @@ -1,5 +1,10 @@ @page "/register" +@using CodeBeam.UltimateAuth.IdentityLab.Contracts +@using CodeBeam.UltimateAuth.IdentityLab.Services @layout PublicLayout +@inject LabRegistrationService RegistrationService +@inject ISnackbar Snackbar +@inject NavigationManager Navigation Create Sandbox · Identity Lab @@ -12,16 +17,12 @@ @@ -71,9 +72,10 @@ Color="Color.Primary" FullWidth="true" Size="Size.Large" - Disabled="true" + Disabled="@_isSubmitting" + OnClick="SubmitAsync" EndIcon="@Icons.Material.Filled.ArrowForward"> - Create Sandbox + @(_isSubmitting ? "Creating..." : "Create Sandbox") @@ -89,9 +91,7 @@ @@ -105,17 +105,13 @@ YOUR IDENTITY PLAYGROUND -