Skip to content

Commit

Permalink
Merge pull request #793 from neozhu/bugfix_fusion
Browse files Browse the repository at this point in the history
Refactor User Tracking and Add Session Cleanup Handler
  • Loading branch information
neozhu authored Nov 1, 2024
2 parents 1025a4b + 991f6d1 commit 3d6613f
Show file tree
Hide file tree
Showing 29 changed files with 390 additions and 267 deletions.
4 changes: 2 additions & 2 deletions src/Server.UI/Components/Breadcrumbs/Breadcrumbs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

@if (OnSaveButtonClick.HasDelegate)
{
<MudLoadingButton StartIcon="@Icons.Material.Filled.Save" Color="Color.Primary" DropShadow="false" Loading="@Saving" Variant="Variant.Outlined" OnClick="@Save">@ConstantString.Save</MudLoadingButton>
<MudLoadingButton StartIcon="@Icons.Material.Filled.Save" Color="Color.Primary" DropShadow="false" Loading="@Saving" OnClick="@Save">@ConstantString.Save</MudLoadingButton>
}
@if (OnGoEditClick.HasDelegate)
{
<MudButton StartIcon="@Icons.Material.Filled.Edit" Color="Color.Primary" Variant="Variant.Outlined" OnClick="@GoEdit">@ConstantString.Edit</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Edit" Color="Color.Primary" OnClick="@GoEdit">@ConstantString.Edit</MudButton>
}
@if (OnDeleteClick.HasDelegate || OnPrintClick.HasDelegate)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Server.UI/Components/Dialogs/ConfirmationDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<MudText>@ContentText</MudText>
</DialogContent>
<DialogActions>
<MudButton StartIcon="@Icons.Material.Filled.Cancel" OnClick="Cancel">@ConstantString.Cancel</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Delete" Color="Color.Error" Variant="Variant.Filled" title="@ConstantString.Confirm" OnClick="Submit">@ConstantString.Confirm</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Cancel" OnClick="Cancel" title="@ConstantString.Cancel">@ConstantString.Cancel</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Delete" Color="Color.Error" title="@ConstantString.Confirm" OnClick="Submit">@ConstantString.Confirm</MudButton>
</DialogActions>
</MudDialog>

Expand Down
4 changes: 2 additions & 2 deletions src/Server.UI/Components/Dialogs/DeleteConfirmation.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<MudText>@ContentText</MudText>
</DialogContent>
<DialogActions>
<MudButton StartIcon="@Icons.Material.Filled.Cancel" OnClick="Cancel">@ConstantString.Cancel</MudButton>
<MudButton StartIcon="@Icons.Material.Outlined.Delete" Color="Color.Error" Variant="Variant.Filled" title="@ConstantString.Delete" OnClick="Submit">@ConstantString.Confirm</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Cancel" OnClick="Cancel" title="@ConstantString.Cancel">@ConstantString.Cancel</MudButton>
<MudButton StartIcon="@Icons.Material.Outlined.Delete" Color="Color.Error" title="@ConstantString.Delete" OnClick="Submit">@ConstantString.Confirm</MudButton>
</DialogActions>
</MudDialog>

Expand Down
4 changes: 2 additions & 2 deletions src/Server.UI/Components/Dialogs/LogoutConfirmation.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<MudText>@ContentText</MudText>
</DialogContent>
<DialogActions>
<MudButton StartIcon="@Icons.Material.Filled.Cancel" OnClick="Cancel">@ConstantString.Cancel</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Logout" Color="@Color" Variant="Variant.Filled" OnClick="Submit">@ConstantString.Logout</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Cancel" OnClick="Cancel" title="@ConstantString.Cancel">@ConstantString.Cancel</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Logout" Color="@Color" title="@ConstantString.Logout" OnClick="Submit">@ConstantString.Logout</MudButton>
</DialogActions>
</MudDialog>

Expand Down
18 changes: 10 additions & 8 deletions src/Server.UI/Components/Fusion/ActiveUserSession.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,47 @@
@inject IUserSessionTracker UserSessionTracker
@inject IStringLocalizer<ActiveUserSession> L

@if (!string.IsNullOrEmpty(userName) && State.Value.Any())
@if (!string.IsNullOrEmpty(currentUserId) && State.Value.Any())
{
<MudAlert Class="mb-2" Severity="MudBlazor.Severity.Error" Variant="Variant.Filled" Dense="true">@Message</MudAlert>
<MudAlert Class="mb-2" Severity="MudBlazor.Severity.Error" Variant="Variant.Outlined" Dense="true">@Message</MudAlert>
}

@code {
[Parameter]
public string PageComponent { get; set; } = nameof(ActiveUserSession);
private string Message => $"{string.Join(", ", State.Value)} {L["has this dialog open."]}";
private string? userName;
private string? currentUserId;
[CascadingParameter]
private Task<AuthenticationState> AuthState { get; set; } = default!;
[Inject] private UIActionTracker UIActionTracker { get; init; } = null!;
private TimeSpan UpdateDelay { get; set; } = TimeSpan.FromSeconds(1);
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
userName = authState.User.GetDisplayName() ?? (authState.User.GetUserName()??string.Empty);
await UserSessionTracker.AddUserSession(PageComponent ?? nameof(ActiveUserSession), userName);
currentUserId = authState.User.GetUserId() ?? string.Empty;
await UserSessionTracker.AddUserSession(PageComponent ?? nameof(ActiveUserSession));


}

protected override ComputedState<string[]>.Options GetStateOptions()
=> new() { UpdateDelayer = new UpdateDelayer(UIActionTracker, UpdateDelay) };

protected override async Task<string[]> ComputeState(CancellationToken cancellationToken)
{
var result = await UserSessionTracker.GetUserSessions(cancellationToken);
var result = await UserSessionTracker.GetUserSessions(PageComponent,cancellationToken);
if (result.Any())
{

return result.Where(x => x.PageComponent == PageComponent).Where(x => x.UserSessions.Any(y => y != userName)).SelectMany(x => x.UserSessions.Where(y => y != userName)).ToArray();
return result.Where(x => x.UserId!=currentUserId).Select(x=>x.DisplayName??x.UserName).ToArray();
}

return Array.Empty<string>();
}

public override async ValueTask DisposeAsync()
{
await UserSessionTracker.RemoveUserSession(PageComponent, userName??string.Empty);
await UserSessionTracker.RemoveUserSession(PageComponent);
GC.Collect();
}
}
31 changes: 13 additions & 18 deletions src/Server.UI/Components/Fusion/OnlineUsersTracker.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@
@using ActualLab.Fusion.Blazor
@using ActualLab.Fusion.UI

@inherits ComputedStateComponent<UserInfo[]>
@inherits ComputedStateComponent<List<SessionInfo>>
@inject IOnlineUserTracker OnlineUserTracker
@inject IStringLocalizer<ActiveUserSession> L
@inject UserProfileStateService UserProfileStateService
@if (State.HasValue && State.LastNonErrorValue.Any())
{
<div class="d-flex flex-row gap-2 my-3 gap-2 my-3">
@foreach (var user in State.LastNonErrorValue.OrderBy(u => u.Name != currentUserName))
@foreach (var user in State.LastNonErrorValue.OrderBy(u => u.UserId != currentUserId))
{
<MudBadge Color="Color.Success" Overlap="false" Dot="true" Bordered="true">
@if (string.IsNullOrEmpty(user.ProfilePictureDataUrl))
{
<MudAvatar title="@user.Name">
@user.Name.First()
<MudAvatar title="@user.UserName">
@user.UserName.First()
</MudAvatar>
}
else
{
<MudAvatar title="@user.Name">
<MudAvatar title="@user.UserName">
<MudImage Src="@user.ProfilePictureDataUrl"></MudImage>
</MudAvatar>
}
Expand All @@ -38,7 +38,7 @@
@code {

private string sessionId = Guid.NewGuid().ToString();
private string? currentUserName;
private string? currentUserId;
[CascadingParameter] private Task<AuthenticationState> AuthState { get; set; } = default!;
[Inject] private UIActionTracker UIActionTracker { get; init; } = null!;
private TimeSpan UpdateDelay { get; set; } = TimeSpan.FromSeconds(5);
Expand All @@ -49,33 +49,28 @@
{
await UserProfileStateService.InitializeAsync(state.User.Identity.Name);
var userProfile = UserProfileStateService.UserProfile;
currentUserName = userProfile.UserName;
await OnlineUserTracker.Add(sessionId, new UserInfo(userProfile.UserId,
currentUserId = userProfile.UserId;
await OnlineUserTracker.Initial(new SessionInfo(userProfile.UserId,
userProfile.UserName,
userProfile.Email,
userProfile.DisplayName ?? string.Empty,
"",
userProfile.TenantId,
userProfile.ProfilePictureDataUrl ?? string.Empty,
userProfile.SuperiorName ?? string.Empty,
userProfile.SuperiorId ?? string.Empty,
userProfile.TenantId ?? string.Empty,
userProfile.TenantName ?? string.Empty,
userProfile.PhoneNumber,
userProfile.AssignedRoles ?? Array.Empty<string>(),
UserPresence.Available));
}
}

protected override ComputedState<UserInfo[]>.Options GetStateOptions()
protected override ComputedState<List<SessionInfo>>.Options GetStateOptions()
=> new() { UpdateDelayer = new UpdateDelayer(UIActionTracker, UpdateDelay) };

protected override Task<UserInfo[]> ComputeState(CancellationToken cancellationToken)
protected override Task<List<SessionInfo>> ComputeState(CancellationToken cancellationToken)
{
return OnlineUserTracker.GetOnlineUsers(cancellationToken);
}

public override async ValueTask DisposeAsync()
{
await OnlineUserTracker.Remove(sessionId);
await OnlineUserTracker.Logout();
GC.Collect();
}
}
2 changes: 1 addition & 1 deletion src/Server.UI/Components/Routes.razor
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large" Class="mb-4" />
<MudText Typo="Typo.h5">@L["You are not authorized to view this page."]</MudText>
<MudText Typo="Typo.body1" Class="mt-2">@L["If you need access to this page, please contact the administrator."]</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4">@L["Contact Administrator"]</MudButton>
<MudButton Color="Color.Primary" Class="mt-4">@L["Contact Administrator"]</MudButton>
</MudPaper>
</NotAuthorized>
</AuthorizeRouteView>
Expand Down
2 changes: 1 addition & 1 deletion src/Server.UI/Components/Shared/CustomError.razor
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</div>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="OnRefresh">@ConstantString.Refresh</MudButton>
<MudButton Color="Color.Default" OnClick="OnRefresh">@ConstantString.Refresh</MudButton>
</DialogActions>
</MudDialog>

Expand Down
16 changes: 7 additions & 9 deletions src/Server.UI/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
using QuestPDF.Infrastructure;
using ActualLab.Fusion;
using Toolbelt.Blazor.Extensions.DependencyInjection;
using ActualLab.Fusion.Extensions;
using CleanArchitecture.Blazor.Server.UI.Middlewares;
using Polly;
using Microsoft.AspNetCore.Components.Server.Circuits;


namespace CleanArchitecture.Blazor.Server.UI;
Expand All @@ -44,6 +44,8 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I
services.AddMudServices(config =>
{
MudGlobal.InputDefaults.ShrinkLabel = true;
//MudGlobal.InputDefaults.Variant = Variant.Outlined;
//MudGlobal.ButtonDefaults.Variant = Variant.Outlined;
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomCenter;
config.SnackbarConfiguration.NewestOnTop = false;
config.SnackbarConfiguration.ShowCloseIcon = true;
Expand All @@ -66,13 +68,10 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I
services.AddHotKeys2();

// Fusion services
services.AddFusion(fusion =>
{
fusion.AddInMemoryKeyValueStore();
fusion.AddService<IUserSessionTracker, UserSessionTracker>();
fusion.AddService<IOnlineUserTracker, OnlineUserTracker>();
});

var fusion = services.AddFusion();
fusion.AddService<IUserSessionTracker, UserSessionTracker>();
fusion.AddService<IOnlineUserTracker, OnlineUserTracker>();
services.AddScoped<CircuitHandler, UserSessionCircuitHandler>();

services.AddScoped<LocalizationCookiesMiddleware>()
.Configure<RequestLocalizationOptions>(options =>
Expand Down Expand Up @@ -150,7 +149,6 @@ public static WebApplication ConfigureServer(this WebApplication app, IConfigura

app.UseStatusCodePagesWithRedirects("/404");
app.MapHealthChecks("/health");

app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
Expand Down
10 changes: 7 additions & 3 deletions src/Server.UI/Hubs/HubClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using CleanArchitecture.Blazor.Infrastructure.Constants.User;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;

Expand Down Expand Up @@ -35,7 +36,8 @@ public HubClient(NavigationManager navigationManager, IHttpContextAccessor httpC
options.Cookies = container;
}).WithAutomaticReconnect().Build();

_hubConnection.ServerTimeout = TimeSpan.FromSeconds(30);
_hubConnection.ServerTimeout = TimeSpan.FromSeconds(20);
_hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(10);

// Observe and await the async result of the event invocation
_hubConnection.On<string, string>(nameof(ISignalRHub.Connect), OnLoginEventAsync);
Expand All @@ -52,6 +54,8 @@ public HubClient(NavigationManager navigationManager, IHttpContextAccessor httpC

_hubConnection.On<string, string, string>(nameof(ISignalRHub.SendPrivateMessage),
async (from, to, message) => await OnMessageReceivedEventAsync(from, message).ConfigureAwait(false));


}

// Handle the result of async event invocations
Expand Down Expand Up @@ -102,7 +106,7 @@ private async Task OnMessageReceivedEventAsync( string from,string message)
await Task.Run(() => MessageReceivedEvent?.Invoke(this, new MessageReceivedEventArgs(from, message))).ConfigureAwait(false);
}
}

public async ValueTask DisposeAsync()
{
try
Expand All @@ -114,7 +118,7 @@ public async ValueTask DisposeAsync()
await _hubConnection.DisposeAsync().ConfigureAwait(false);
}
}

// Event handlers
public event EventHandler<UserStateChangeEventArgs>? LoginEvent;
public event EventHandler<UserStateChangeEventArgs>? LogoutEvent;
Expand Down
57 changes: 57 additions & 0 deletions src/Server.UI/Middlewares/UserSessionCircuitHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using CleanArchitecture.Blazor.Server.UI.Services.Fusion;
using Microsoft.AspNetCore.Components.Server.Circuits;

namespace CleanArchitecture.Blazor.Server.UI.Middlewares;

/// <summary>
/// Handles user session tracking and online user tracking for Blazor server circuits.
/// </summary>
public class UserSessionCircuitHandler : CircuitHandler
{
private readonly IUserSessionTracker _userSessionTracker;
private readonly IOnlineUserTracker _onlineUserTracker;
private readonly IHttpContextAccessor _httpContextAccessor;

/// <summary>
/// Initializes a new instance of the <see cref="UserSessionCircuitHandler"/> class.
/// </summary>
/// <param name="userSessionTracker">The user session tracker service.</param>
/// <param name="onlineUserTracker">The online user tracker service.</param>
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
public UserSessionCircuitHandler(IUserSessionTracker userSessionTracker, IOnlineUserTracker onlineUserTracker, IHttpContextAccessor httpContextAccessor)
{
_userSessionTracker = userSessionTracker;
_onlineUserTracker = onlineUserTracker;
_httpContextAccessor = httpContextAccessor;
}

/// <summary>
/// Called when a new circuit connection is established.
/// </summary>
/// <param name="circuit">The circuit.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public override async Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
await base.OnConnectionUpAsync(circuit, cancellationToken);
}

/// <summary>
/// Called when a circuit connection is disconnected.
/// </summary>
/// <param name="circuit">The circuit.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public override async Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
var userId = _httpContextAccessor.HttpContext?.User.GetUserId();

if (!string.IsNullOrEmpty(userId))
{
await _userSessionTracker.RemoveAllSessions(userId, cancellationToken);
await _onlineUserTracker.Clear(userId, cancellationToken);
}

await base.OnConnectionDownAsync(circuit, cancellationToken);
}
}
6 changes: 3 additions & 3 deletions src/Server.UI/Pages/Contacts/Contacts.razor
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,21 @@
</MudStack>
<MudStack Spacing="0" AlignItems="AlignItems.End">
<MudToolBar Dense WrapContent="true" Class="py-1 px-0">
<MudButton Variant="Variant.Outlined"
<MudButton
Disabled="@_loading"
OnClick="@(() => OnRefresh())"
StartIcon="@Icons.Material.Outlined.Refresh">
@ConstantString.Refresh
</MudButton>
@if (_canCreate)
{
<MudButton Variant="Variant.Outlined"
<MudButton
StartIcon="@Icons.Material.Outlined.Add"
OnClick="OnCreate">
@ConstantString.New
</MudButton>
}
<MudMenu Variant="Variant.Outlined" TransformOrigin="Origin.BottomRight" AnchorOrigin="Origin.BottomRight" EndIcon="@Icons.Material.Filled.MoreVert" Label="@ConstantString.More">
<MudMenu TransformOrigin="Origin.BottomRight" AnchorOrigin="Origin.BottomRight" EndIcon="@Icons.Material.Filled.MoreVert" Label="@ConstantString.More">
@if (_canCreate)
{
<MudMenuItem Disabled="@(_selectedItems.Count != 1)" OnClick="OnClone">@ConstantString.Clone</MudMenuItem>
Expand Down
2 changes: 1 addition & 1 deletion src/Server.UI/Pages/Contacts/CreateContact.razor
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

</MudCardContent>
<MudCardActions Class="d-flex justify-end gap-2">
<MudLoadingButton Color="Color.Primary" DropShadow="false" Loading="@_saving" Variant="Variant.Outlined" OnClick="Submit">@ConstantString.Save</MudLoadingButton>
<MudLoadingButton Color="Color.Primary" DropShadow="false" Loading="@_saving" OnClick="Submit">@ConstantString.Save</MudLoadingButton>
</MudCardActions>
</MudCard>
</MudContainer>
Expand Down
2 changes: 1 addition & 1 deletion src/Server.UI/Pages/Contacts/EditContact.razor
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

</MudCardContent>
<MudCardActions Class="d-flex justify-end gap-2">
<MudLoadingButton Color="Color.Primary" DropShadow="false" Loading="@_saving" Variant="Variant.Outlined" OnClick="Submit">@ConstantString.Save</MudLoadingButton>
<MudLoadingButton Color="Color.Primary" DropShadow="false" Loading="@_saving" OnClick="Submit">@ConstantString.Save</MudLoadingButton>
</MudCardActions>
</MudCard>
}
Expand Down
Loading

0 comments on commit 3d6613f

Please sign in to comment.