mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 11:44:56 +00:00
Refactor user ID access in SignalR hubs and add online user tracking
Introduces IHubUserAccessor and its implementation to centralize user ID retrieval from SignalR HubCallerContext, replacing duplicated logic in ChatsHub, FriendsHub, and PresenceHub. Moves extension and mapping files to a Common directory, adds UserToUserDtoMappingAction for online status mapping, and implements OnlineUserStore with tests for tracking online users. Updates dependency injection and test code to use the new abstractions.
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace Govor.API.Tests.Common.SignalR.Helpers;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[TestOf(typeof(HubUserAccessor))]
|
||||||
|
public class HubUserAccessorTests
|
||||||
|
{
|
||||||
|
private HubUserAccessor _accessor;
|
||||||
|
private Mock<ILogger<HubUserAccessor>> _loggerMock;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_loggerMock = new Mock<ILogger<HubUserAccessor>>();
|
||||||
|
_accessor = new HubUserAccessor(_loggerMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HubCallerContext CreateContextWithClaims(params Claim[] claims)
|
||||||
|
{
|
||||||
|
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||||
|
var contextMock = new Mock<HubCallerContext>();
|
||||||
|
contextMock.Setup(c => c.User).Returns(principal);
|
||||||
|
return contextMock.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetUserId_ValidClaim_ReturnsGuid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedGuid = Guid.NewGuid();
|
||||||
|
var context = CreateContextWithClaims(new Claim("userId", expectedGuid.ToString()));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _accessor.GetUserId(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(expectedGuid, Is.EqualTo(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetUserId_InvalidClaim_ThrowsException_WhenNotSuppressed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContextWithClaims(new Claim("userId", "not-a-guid"));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(context, suppressException: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetUserId_InvalidClaim_ReturnsEmptyGuid_WhenSuppressed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContextWithClaims(new Claim("userId", "not-a-guid"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _accessor.GetUserId(context, suppressException: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(Guid.Empty, Is.EqualTo(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetUserId_NoClaim_ThrowsException_WhenNotSuppressed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContextWithClaims();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(context, suppressException: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetUserId_NoClaim_ReturnsEmptyGuid_WhenSuppressed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContextWithClaims();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _accessor.GetUserId(context, suppressException: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(Guid.Empty, Is.EqualTo(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetUserId_UserIsNull_ReturnsEmptyGuid_WhenSuppressed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var contextMock = new Mock<HubCallerContext>();
|
||||||
|
contextMock.Setup(c => c.User).Returns((ClaimsPrincipal?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _accessor.GetUserId(contextMock.Object, suppressException: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(Guid.Empty, Is.EqualTo(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetUserId_UserIsNull_ThrowsException_WhenNotSuppressed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var contextMock = new Mock<HubCallerContext>();
|
||||||
|
contextMock.Setup(c => c.User).Returns((ClaimsPrincipal?)null);
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(contextMock.Object, suppressException: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
using Govor.API.Hubs;
|
using Govor.API.Hubs;
|
||||||
using Govor.Application.Interfaces;
|
using Govor.Application.Interfaces;
|
||||||
using Govor.Application.Interfaces.Messages;
|
using Govor.Application.Interfaces.Messages;
|
||||||
@@ -13,6 +14,7 @@ public class ChatsHubTests
|
|||||||
private Mock<ILogger<ChatsHub>> _loggerMock;
|
private Mock<ILogger<ChatsHub>> _loggerMock;
|
||||||
private Mock<IMessageCommandService> _messageServiceMock;
|
private Mock<IMessageCommandService> _messageServiceMock;
|
||||||
private Mock<IUserGroupsService> _userGroupsServiceMock;
|
private Mock<IUserGroupsService> _userGroupsServiceMock;
|
||||||
|
private Mock<IHubUserAccessor> _hubUserAccessorMock;
|
||||||
private Fixture _fixture;
|
private Fixture _fixture;
|
||||||
private ChatsHub _chatsHub;
|
private ChatsHub _chatsHub;
|
||||||
|
|
||||||
@@ -26,11 +28,13 @@ public class ChatsHubTests
|
|||||||
_messageServiceMock = new Mock<IMessageCommandService>();
|
_messageServiceMock = new Mock<IMessageCommandService>();
|
||||||
_userGroupsServiceMock = new Mock<IUserGroupsService>();
|
_userGroupsServiceMock = new Mock<IUserGroupsService>();
|
||||||
_loggerMock = new Mock<ILogger<ChatsHub>>();
|
_loggerMock = new Mock<ILogger<ChatsHub>>();
|
||||||
|
_hubUserAccessorMock = new Mock<IHubUserAccessor>();
|
||||||
|
|
||||||
_chatsHub = new ChatsHub(
|
_chatsHub = new ChatsHub(
|
||||||
_loggerMock.Object,
|
_loggerMock.Object,
|
||||||
_messageServiceMock.Object,
|
_messageServiceMock.Object,
|
||||||
_userGroupsServiceMock.Object
|
_userGroupsServiceMock.Object,
|
||||||
|
_hubUserAccessorMock.Object
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
using Govor.API.Hubs;
|
using Govor.API.Hubs;
|
||||||
using Govor.Application.Exceptions.FriendsService;
|
using Govor.Application.Exceptions.FriendsService;
|
||||||
using Govor.Application.Interfaces.Friends;
|
using Govor.Application.Interfaces.Friends;
|
||||||
@@ -17,7 +18,7 @@ namespace Govor.API.Tests.IntegrationTests.Hubs;
|
|||||||
public class FriendsHubTests
|
public class FriendsHubTests
|
||||||
{
|
{
|
||||||
private Mock<IFriendRequestCommandService> _friendRequestServiceMock = null!;
|
private Mock<IFriendRequestCommandService> _friendRequestServiceMock = null!;
|
||||||
private Mock<ICurrentUserService> _currentUserServiceMock = null!;
|
private Mock<IHubUserAccessor> _currentUserServiceMock = null!;
|
||||||
private Mock<IHubCallerClients> _clientsMock = null!;
|
private Mock<IHubCallerClients> _clientsMock = null!;
|
||||||
private Mock<IClientProxy> _clientProxyMock = null!;
|
private Mock<IClientProxy> _clientProxyMock = null!;
|
||||||
private Mock<ILogger<FriendsHub>> _loggerMock = null!;
|
private Mock<ILogger<FriendsHub>> _loggerMock = null!;
|
||||||
@@ -35,19 +36,23 @@ public class FriendsHubTests
|
|||||||
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
|
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
|
||||||
|
|
||||||
_friendRequestServiceMock = new Mock<IFriendRequestCommandService>();
|
_friendRequestServiceMock = new Mock<IFriendRequestCommandService>();
|
||||||
_currentUserServiceMock = new Mock<ICurrentUserService>();
|
_currentUserServiceMock = new Mock<IHubUserAccessor>();
|
||||||
_clientsMock = new Mock<IHubCallerClients>();
|
_clientsMock = new Mock<IHubCallerClients>();
|
||||||
_clientProxyMock = new Mock<IClientProxy>();
|
_clientProxyMock = new Mock<IClientProxy>();
|
||||||
_loggerMock = new Mock<ILogger<FriendsHub>>();
|
_loggerMock = new Mock<ILogger<FriendsHub>>();
|
||||||
_mapperMock = new Mock<IMapper>();
|
_mapperMock = new Mock<IMapper>();
|
||||||
|
|
||||||
_currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Returns(_userId);
|
_currentUserServiceMock.Setup(x => x.GetUserId(
|
||||||
|
It.IsAny<HubCallerContext>(),
|
||||||
|
It.IsAny<bool>()))
|
||||||
|
.Returns(_userId);
|
||||||
|
|
||||||
_clientsMock.Setup(c => c.Group(It.IsAny<string>())).Returns(_clientProxyMock.Object);
|
_clientsMock.Setup(c => c.Group(It.IsAny<string>())).Returns(_clientProxyMock.Object);
|
||||||
|
|
||||||
_hub = new FriendsHub(
|
_hub = new FriendsHub(
|
||||||
|
_loggerMock.Object,
|
||||||
_friendRequestServiceMock.Object,
|
_friendRequestServiceMock.Object,
|
||||||
_currentUserServiceMock.Object,
|
_currentUserServiceMock.Object,
|
||||||
_loggerMock.Object,
|
|
||||||
_mapperMock.Object)
|
_mapperMock.Object)
|
||||||
{
|
{
|
||||||
Clients = _clientsMock.Object
|
Clients = _clientsMock.Object
|
||||||
@@ -129,7 +134,7 @@ public class FriendsHubTests
|
|||||||
public async Task SendRequest_ShouldReturnUnauthorized_WhenCurrentUserIsNotAuthenticated()
|
public async Task SendRequest_ShouldReturnUnauthorized_WhenCurrentUserIsNotAuthenticated()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_currentUserServiceMock.Setup(x => x.GetCurrentUserId())
|
_currentUserServiceMock.Setup(x => x.GetUserId(It.IsAny<HubCallerContext>(), It.IsAny<bool>()))
|
||||||
.Throws(new UnauthorizedAccessException("userId claim is missing or invalid"));
|
.Throws(new UnauthorizedAccessException("userId claim is missing or invalid"));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using Govor.Application.Services.Authentication;
|
using Govor.Application.Services.Authentication;
|
||||||
|
|
||||||
namespace Govor.API.Extensions;
|
namespace Govor.API.Common.Extensions;
|
||||||
|
|
||||||
public static class AddOptionExtensions
|
public static class AddOptionExtensions
|
||||||
{
|
{
|
||||||
+5
-1
@@ -1,3 +1,5 @@
|
|||||||
|
using Govor.API.Common.Mapping;
|
||||||
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
using Govor.Application.Infrastructure.AdminsStuff;
|
using Govor.Application.Infrastructure.AdminsStuff;
|
||||||
using Govor.Application.Infrastructure.Extensions;
|
using Govor.Application.Infrastructure.Extensions;
|
||||||
using Govor.Application.Infrastructure.Validators;
|
using Govor.Application.Infrastructure.Validators;
|
||||||
@@ -34,7 +36,7 @@ using Govor.Data;
|
|||||||
using Govor.Data.Repositories;
|
using Govor.Data.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Govor.API.Extensions;
|
namespace Govor.API.Common.Extensions;
|
||||||
|
|
||||||
public static class ConfigurationProgramExtensions
|
public static class ConfigurationProgramExtensions
|
||||||
{
|
{
|
||||||
@@ -83,6 +85,8 @@ public static class ConfigurationProgramExtensions
|
|||||||
|
|
||||||
// Auto Mapper
|
// Auto Mapper
|
||||||
services.AddAutoMapper(typeof(MappingProfile));
|
services.AddAutoMapper(typeof(MappingProfile));
|
||||||
|
|
||||||
|
services.AddScoped<IHubUserAccessor, HubUserAccessor>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddRepositories(this IServiceCollection services)
|
public static void AddRepositories(this IServiceCollection services)
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using Govor.API.Filters;
|
using Govor.API.Filters;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace Govor.API.Extensions;
|
namespace Govor.API.Common.Extensions;
|
||||||
|
|
||||||
public static class ConfigurationSignalR
|
public static class ConfigurationSignalR
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Govor.API.Extensions;
|
namespace Govor.API.Common.Extensions;
|
||||||
|
|
||||||
public static class ConfiguratorLoggerExtensions
|
public static class ConfiguratorLoggerExtensions
|
||||||
{
|
{
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Govor.API.Extensions.Mapping;
|
||||||
using Govor.Contracts.DTOs;
|
using Govor.Contracts.DTOs;
|
||||||
using Govor.Contracts.Responses;
|
using Govor.Contracts.Responses;
|
||||||
using Govor.Core.Models;
|
using Govor.Core.Models;
|
||||||
using Govor.Core.Models.Messages;
|
using Govor.Core.Models.Messages;
|
||||||
using Govor.Core.Models.Users;
|
using Govor.Core.Models.Users;
|
||||||
|
|
||||||
namespace Govor.API.Extensions;
|
namespace Govor.API.Common.Mapping;
|
||||||
|
|
||||||
public class MappingProfile : Profile
|
public class MappingProfile : Profile
|
||||||
{
|
{
|
||||||
@@ -16,7 +17,9 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<MessageReaction, MessageReactionResponse>();
|
CreateMap<MessageReaction, MessageReactionResponse>();
|
||||||
CreateMap<MessageView, MessageViewResponse>();
|
CreateMap<MessageView, MessageViewResponse>();
|
||||||
|
|
||||||
CreateMap<User, UserDto>();
|
CreateMap<User, UserDto>()
|
||||||
|
.AfterMap<UserToUserDtoMappingAction>();
|
||||||
|
|
||||||
CreateMap<Friendship, FriendshipDto>();
|
CreateMap<Friendship, FriendshipDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Govor.Application.Interfaces.UserOnlineStatus;
|
||||||
|
using Govor.Contracts.DTOs;
|
||||||
|
using Govor.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace Govor.API.Extensions.Mapping;
|
||||||
|
|
||||||
|
public class UserToUserDtoMappingAction : IMappingAction<User, UserDto>
|
||||||
|
{
|
||||||
|
private readonly IOnlineUserStore _onlineUserStore;
|
||||||
|
|
||||||
|
public UserToUserDtoMappingAction(IOnlineUserStore onlineUserStore)
|
||||||
|
{
|
||||||
|
_onlineUserStore = onlineUserStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Process(User source, UserDto destination, ResolutionContext context)
|
||||||
|
{
|
||||||
|
destination.IsOnline = _onlineUserStore.IsOnline(source.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Govor.API.Common.SignalR.Helpers;
|
||||||
|
|
||||||
|
public class HubUserAccessor : IHubUserAccessor
|
||||||
|
{
|
||||||
|
private readonly ILogger<HubUserAccessor> _logger;
|
||||||
|
|
||||||
|
public HubUserAccessor(ILogger<HubUserAccessor> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid GetUserId(HubCallerContext context, bool suppressException = false)
|
||||||
|
{
|
||||||
|
var userIdClaim = context.User?.FindFirst("userId")?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
if (!suppressException)
|
||||||
|
{
|
||||||
|
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
|
||||||
|
throw new UnauthorizedAccessException("userID claim is missing or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Govor.API.Common.SignalR.Helpers;
|
||||||
|
|
||||||
|
public interface IHubUserAccessor
|
||||||
|
{
|
||||||
|
Guid GetUserId(HubCallerContext context, bool suppressException = false);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
using Govor.Application.Exceptions.VerifyFriendship;
|
using Govor.Application.Exceptions.VerifyFriendship;
|
||||||
using Govor.Application.Interfaces;
|
using Govor.Application.Interfaces;
|
||||||
using Govor.Application.Interfaces.Messages;
|
using Govor.Application.Interfaces.Messages;
|
||||||
@@ -16,17 +17,19 @@ public class ChatsHub : Hub
|
|||||||
private readonly ILogger<ChatsHub> _logger;
|
private readonly ILogger<ChatsHub> _logger;
|
||||||
private readonly IMessageCommandService _messageCommandService;
|
private readonly IMessageCommandService _messageCommandService;
|
||||||
private readonly IUserGroupsService _userService;
|
private readonly IUserGroupsService _userService;
|
||||||
|
private readonly IHubUserAccessor _userAccessor;
|
||||||
|
|
||||||
public ChatsHub(ILogger<ChatsHub> logger, IMessageCommandService messageCommandService, IUserGroupsService userService)
|
public ChatsHub(ILogger<ChatsHub> logger, IMessageCommandService messageCommandService, IUserGroupsService userService, IHubUserAccessor userAccessor)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_messageCommandService = messageCommandService;
|
_messageCommandService = messageCommandService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_userAccessor = userAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
if (userId == Guid.Empty)
|
if (userId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User connected with invalid UserID claim.");
|
_logger.LogWarning("User connected with invalid UserID claim.");
|
||||||
@@ -50,8 +53,7 @@ public class ChatsHub : Hub
|
|||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
var userId =
|
var userId = _userAccessor.GetUserId(Context, true);
|
||||||
GetUserId(suppressException: true);
|
|
||||||
if (userId != Guid.Empty)
|
if (userId != Guid.Empty)
|
||||||
{
|
{
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
|
||||||
@@ -85,7 +87,7 @@ public class ChatsHub : Hub
|
|||||||
|
|
||||||
public async Task<HubResult<UserMessageResponse>> Send(MessageRequest request)
|
public async Task<HubResult<UserMessageResponse>> Send(MessageRequest request)
|
||||||
{
|
{
|
||||||
var senderId = GetUserId();
|
var senderId= _userAccessor.GetUserId(Context);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
|
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
|
||||||
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
||||||
@@ -144,7 +146,7 @@ public class ChatsHub : Hub
|
|||||||
|
|
||||||
public async Task<HubResult<MessageRemovedResponse>> Remove(RemoveMessageRequest request)
|
public async Task<HubResult<MessageRemovedResponse>> Remove(RemoveMessageRequest request)
|
||||||
{
|
{
|
||||||
var removerId = GetUserId();
|
var removerId = _userAccessor.GetUserId(Context);
|
||||||
_logger.LogInformation("Removing message {MessageId} by user {RemoverId}", request.MessageId, removerId);
|
_logger.LogInformation("Removing message {MessageId} by user {RemoverId}", request.MessageId, removerId);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -184,7 +186,7 @@ public class ChatsHub : Hub
|
|||||||
|
|
||||||
public async Task<HubResult<MessageEditResponse>> Edit(EditMessageRequest request)
|
public async Task<HubResult<MessageEditResponse>> Edit(EditMessageRequest request)
|
||||||
{
|
{
|
||||||
var editor = GetUserId();
|
var editor = _userAccessor.GetUserId(Context);
|
||||||
_logger.LogInformation("Editing message {MessageId} by user {EditorId}", request.MessageId, editor);
|
_logger.LogInformation("Editing message {MessageId} by user {EditorId}", request.MessageId, editor);
|
||||||
|
|
||||||
var editMessageParam = new EditMessage(editor,
|
var editMessageParam = new EditMessage(editor,
|
||||||
@@ -303,22 +305,4 @@ public class ChatsHub : Hub
|
|||||||
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
|
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
|
||||||
return HubResult<T>.NotFound("Message not found.");
|
return HubResult<T>.NotFound("Message not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Guid GetUserId(bool suppressException = false)
|
|
||||||
{
|
|
||||||
var userIdClaim = Context.User?.FindFirst("userId")?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
|
||||||
{
|
|
||||||
if (!suppressException)
|
|
||||||
{
|
|
||||||
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
|
|
||||||
throw new UnauthorizedAccessException("userID claim is missing or invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Guid.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
using Govor.Application.Exceptions.FriendsService;
|
using Govor.Application.Exceptions.FriendsService;
|
||||||
using Govor.Application.Interfaces.Friends;
|
using Govor.Application.Interfaces.Friends;
|
||||||
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
||||||
@@ -13,23 +14,24 @@ public class FriendsHub : Hub
|
|||||||
{
|
{
|
||||||
private readonly ILogger<FriendsHub> _logger;
|
private readonly ILogger<FriendsHub> _logger;
|
||||||
private readonly IFriendRequestCommandService _friendRequestService;
|
private readonly IFriendRequestCommandService _friendRequestService;
|
||||||
private readonly ICurrentUserService _currentUserService;
|
private readonly IHubUserAccessor _userAccessor;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
public FriendsHub(IFriendRequestCommandService friendRequestService,
|
public FriendsHub(
|
||||||
ICurrentUserService currentUserService,
|
|
||||||
ILogger<FriendsHub> logger,
|
ILogger<FriendsHub> logger,
|
||||||
|
IFriendRequestCommandService friendRequestService,
|
||||||
|
IHubUserAccessor userAccessor,
|
||||||
IMapper mapper)
|
IMapper mapper)
|
||||||
{
|
{
|
||||||
_friendRequestService = friendRequestService;
|
|
||||||
_currentUserService = currentUserService;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_friendRequestService = friendRequestService;
|
||||||
|
_userAccessor = userAccessor;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
if (userId == Guid.Empty)
|
if (userId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User connected with invalid UserID claim.");
|
_logger.LogWarning("User connected with invalid UserID claim.");
|
||||||
@@ -46,8 +48,7 @@ public class FriendsHub : Hub
|
|||||||
}
|
}
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
var userId =
|
var userId = _userAccessor.GetUserId(Context, true);
|
||||||
GetUserId(suppressException: true);
|
|
||||||
if (userId != Guid.Empty)
|
if (userId != Guid.Empty)
|
||||||
{
|
{
|
||||||
// Remove user from their own group
|
// Remove user from their own group
|
||||||
@@ -76,7 +77,7 @@ public class FriendsHub : Hub
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = _currentUserService.GetCurrentUserId();
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
var friendship = await _friendRequestService.SendAsync(userId, targetUserId);
|
var friendship = await _friendRequestService.SendAsync(userId, targetUserId);
|
||||||
|
|
||||||
await Clients.Group(targetUserId.ToString())
|
await Clients.Group(targetUserId.ToString())
|
||||||
@@ -111,7 +112,7 @@ public class FriendsHub : Hub
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = _currentUserService.GetCurrentUserId();
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
var friendship = await _friendRequestService.AcceptAsync(friendshipId, userId);
|
var friendship = await _friendRequestService.AcceptAsync(friendshipId, userId);
|
||||||
await Clients.Group(userId.ToString())
|
await Clients.Group(userId.ToString())
|
||||||
.SendAsync("FriendRequestAccepted", _mapper.Map<FriendshipDto>(friendship));
|
.SendAsync("FriendRequestAccepted", _mapper.Map<FriendshipDto>(friendship));
|
||||||
@@ -140,7 +141,7 @@ public class FriendsHub : Hub
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = _currentUserService.GetCurrentUserId();
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
var friendship = await _friendRequestService.RejectAsync(friendshipId, userId);
|
var friendship = await _friendRequestService.RejectAsync(friendshipId, userId);
|
||||||
await Clients.Group(userId.ToString())
|
await Clients.Group(userId.ToString())
|
||||||
.SendAsync("FriendRequestRejected", _mapper.Map<FriendshipDto>(friendship));
|
.SendAsync("FriendRequestRejected", _mapper.Map<FriendshipDto>(friendship));
|
||||||
@@ -164,21 +165,4 @@ public class FriendsHub : Hub
|
|||||||
return HubResult<object>.Error("Unexpected error! Please try later!");
|
return HubResult<object>.Error("Unexpected error! Please try later!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid GetUserId(bool suppressException = false)
|
|
||||||
{
|
|
||||||
var userIdClaim = Context.User?.FindFirst("userId")?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
|
||||||
{
|
|
||||||
if (!suppressException)
|
|
||||||
{
|
|
||||||
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
|
|
||||||
throw new UnauthorizedAccessException("userID claim is missing or invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Guid.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Govor.Application.Interfaces;
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
|
||||||
using Govor.Application.Interfaces.UserOnlineStatus;
|
using Govor.Application.Interfaces.UserOnlineStatus;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@@ -14,17 +13,23 @@ public class PresenceHub : Hub
|
|||||||
private readonly ILogger<PresenceHub> _logger;
|
private readonly ILogger<PresenceHub> _logger;
|
||||||
private readonly IUserNotificationScopeService _notificationScopeService;
|
private readonly IUserNotificationScopeService _notificationScopeService;
|
||||||
private readonly IOnlineUserStore _onlineUserStore;
|
private readonly IOnlineUserStore _onlineUserStore;
|
||||||
|
private readonly IHubUserAccessor _userAccessor;
|
||||||
|
|
||||||
public PresenceHub(ILogger<PresenceHub> logger, IUserNotificationScopeService notificationScopeService, IOnlineUserStore onlineUserStore)
|
public PresenceHub(
|
||||||
|
ILogger<PresenceHub> logger,
|
||||||
|
IUserNotificationScopeService notificationScopeService,
|
||||||
|
IOnlineUserStore onlineUserStore,
|
||||||
|
IHubUserAccessor userAccessor)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_notificationScopeService = notificationScopeService;
|
_notificationScopeService = notificationScopeService;
|
||||||
_onlineUserStore = onlineUserStore;
|
_onlineUserStore = onlineUserStore;
|
||||||
|
_userAccessor = userAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
if (userId == Guid.Empty)
|
if (userId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User connected with invalid UserID claim.");
|
_logger.LogWarning("User connected with invalid UserID claim.");
|
||||||
@@ -48,7 +53,7 @@ public class PresenceHub : Hub
|
|||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
var userId = _userAccessor.GetUserId(Context, true);
|
||||||
if (userId == Guid.Empty) return;
|
if (userId == Guid.Empty) return;
|
||||||
|
|
||||||
_onlineUserStore.SetOfflineUser(userId);
|
_onlineUserStore.SetOfflineUser(userId);
|
||||||
@@ -63,21 +68,4 @@ public class PresenceHub : Hub
|
|||||||
|
|
||||||
await base.OnDisconnectedAsync(exception);
|
await base.OnDisconnectedAsync(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid GetUserId(bool suppressException = false)
|
|
||||||
{
|
|
||||||
var userIdClaim = Context.User?.FindFirst("userId")?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
|
||||||
{
|
|
||||||
if (!suppressException)
|
|
||||||
{
|
|
||||||
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
|
|
||||||
throw new UnauthorizedAccessException("userId claim is missing or invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Guid.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Govor.API.Extensions;
|
using Govor.API.Common.Extensions;
|
||||||
using Govor.API.Hubs;
|
using Govor.API.Hubs;
|
||||||
using Govor.Application.Services.Authentication;
|
using Govor.Application.Services.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Govor.Application.Interfaces.UserOnlineStatus;
|
||||||
|
using Govor.Application.Services.UserOnlineStatus;
|
||||||
|
|
||||||
|
namespace Govor.Application.Tests.Services.UserOnlineStatus;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class OnlineUserStoreTests
|
||||||
|
{
|
||||||
|
private IOnlineUserStore _store;
|
||||||
|
private Guid _userId1;
|
||||||
|
private Guid _userId2;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_store = new OnlineUserStore();
|
||||||
|
_userId1 = Guid.NewGuid();
|
||||||
|
_userId2 = Guid.NewGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetOnlineUser_UserIsMarkedOnline()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
_store.SetOnlineUser(_userId1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(_store.IsOnline(_userId1), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetOfflineUser_UserIsNoLongerOnline()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
_store.SetOnlineUser(_userId1);
|
||||||
|
_store.SetOfflineUser(_userId1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(_store.IsOnline(_userId1), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsOnline_ReturnsFalse_ForUnknownUser()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.That(_store.IsOnline(Guid.NewGuid()), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetAllOnlineUsers_ReturnsAllCurrentlyOnlineUsers()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_store.SetOnlineUser(_userId1);
|
||||||
|
_store.SetOnlineUser(_userId2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var onlineUsers = _store.GetAllOnlineUsers();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(onlineUsers, Is.EquivalentTo(new[] { _userId1, _userId2 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetOnlineUser_Twice_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.DoesNotThrow(() =>
|
||||||
|
{
|
||||||
|
_store.SetOnlineUser(_userId1);
|
||||||
|
_store.SetOnlineUser(_userId1);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(_store.IsOnline(_userId1), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetOfflineUser_ForUnknownUser_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.DoesNotThrow(() => _store.SetOfflineUser(Guid.NewGuid()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,29 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using Govor.Application.Interfaces.UserOnlineStatus;
|
using Govor.Application.Interfaces.UserOnlineStatus;
|
||||||
|
|
||||||
namespace Govor.Application.Services.UserOnlineStatus;
|
namespace Govor.Application.Services.UserOnlineStatus;
|
||||||
|
|
||||||
public class OnlineUserStore : IOnlineUserStore
|
public class OnlineUserStore : IOnlineUserStore
|
||||||
{
|
{
|
||||||
|
private readonly ConcurrentDictionary<Guid, DateTime> _onlineUsers = new();
|
||||||
|
|
||||||
public void SetOnlineUser(Guid userId)
|
public void SetOnlineUser(Guid userId)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
_onlineUsers[userId] = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetOfflineUser(Guid userId)
|
public void SetOfflineUser(Guid userId)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
_onlineUsers.TryRemove(userId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsOnline(Guid userId)
|
public bool IsOnline(Guid userId)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
return _onlineUsers.ContainsKey(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyCollection<Guid> GetAllOnlineUsers()
|
public IReadOnlyCollection<Guid> GetAllOnlineUsers()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
return _onlineUsers.Keys.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user