mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 11:44:56 +00:00
test to make server
This commit is contained in:
@@ -14,7 +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<IUserGroupsGetterService> _userGroupsServiceMock;
|
||||||
private Mock<IHubUserAccessor> _hubUserAccessorMock;
|
private Mock<IHubUserAccessor> _hubUserAccessorMock;
|
||||||
private Fixture _fixture;
|
private Fixture _fixture;
|
||||||
private ChatsHub _chatsHub;
|
private ChatsHub _chatsHub;
|
||||||
@@ -27,7 +27,7 @@ public class ChatsHubTests
|
|||||||
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
|
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
|
||||||
|
|
||||||
_messageServiceMock = new Mock<IMessageCommandService>();
|
_messageServiceMock = new Mock<IMessageCommandService>();
|
||||||
_userGroupsServiceMock = new Mock<IUserGroupsService>();
|
_userGroupsServiceMock = new Mock<IUserGroupsGetterService>();
|
||||||
_loggerMock = new Mock<ILogger<ChatsHub>>();
|
_loggerMock = new Mock<ILogger<ChatsHub>>();
|
||||||
_hubUserAccessorMock = new Mock<IHubUserAccessor>();
|
_hubUserAccessorMock = new Mock<IHubUserAccessor>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Govor.API.Common.Mapping;
|
using Govor.API.Common.Mapping;
|
||||||
using Govor.API.Common.SignalR.Helpers;
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
|
using Govor.API.Hubs.Infrastructure;
|
||||||
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;
|
||||||
@@ -52,6 +53,7 @@ public static class ConfigurationProgramExtensions
|
|||||||
services.AddScoped<IInvitesService, InvitesService>();
|
services.AddScoped<IInvitesService, InvitesService>();
|
||||||
services.AddScoped<IInvitationGenerator, InvitationGenerator>();
|
services.AddScoped<IInvitationGenerator, InvitationGenerator>();
|
||||||
services.AddScoped<IUsernameValidator, UsernameValidator>();
|
services.AddScoped<IUsernameValidator, UsernameValidator>();
|
||||||
|
services.AddScoped<ISynchingService, SynchingService>();
|
||||||
|
|
||||||
// Friends services
|
// Friends services
|
||||||
services.AddScoped<IFriendshipService, FriendshipService>();
|
services.AddScoped<IFriendshipService, FriendshipService>();
|
||||||
@@ -74,7 +76,8 @@ public static class ConfigurationProgramExtensions
|
|||||||
|
|
||||||
services.AddScoped<IMessageCommandService, MessageCommandService>();
|
services.AddScoped<IMessageCommandService, MessageCommandService>();
|
||||||
services.AddScoped<IVerifyFriendship, VerifyFriendship>();
|
services.AddScoped<IVerifyFriendship, VerifyFriendship>();
|
||||||
services.AddScoped<IUserGroupsService, UserGroupsService>();
|
services.AddScoped<IUserGroupsGetterService, UserGroupsGetterService>();
|
||||||
|
services.AddScoped<IUserPrivateChatsGetterService, UserPrivateChatsGetter>();
|
||||||
services.AddScoped<IMessagesLoader, MessagesLoader>();
|
services.AddScoped<IMessagesLoader, MessagesLoader>();
|
||||||
services.AddScoped<IMediaService, MediaService>();
|
services.AddScoped<IMediaService, MediaService>();
|
||||||
services.AddScoped<IAccesserToDownloadMedia, AccesserToDownloadMediaService>();
|
services.AddScoped<IAccesserToDownloadMedia, AccesserToDownloadMediaService>();
|
||||||
@@ -87,6 +90,10 @@ public static class ConfigurationProgramExtensions
|
|||||||
services.AddScoped<IUserPresenceReader, UserPresenceReader>();
|
services.AddScoped<IUserPresenceReader, UserPresenceReader>();
|
||||||
services.AddSingleton<IOnlineUserStore, OnlineUserStore>();
|
services.AddSingleton<IOnlineUserStore, OnlineUserStore>();
|
||||||
|
|
||||||
|
// Hubs Infrastructure
|
||||||
|
services.AddScoped<IChatNotificationService, ChatNotificationService>();
|
||||||
|
services.AddScoped<IConnectionManager, ConnectionManager>();
|
||||||
|
|
||||||
// Auto Mapper
|
// Auto Mapper
|
||||||
services.AddAutoMapper(typeof(MappingProfile));
|
services.AddAutoMapper(typeof(MappingProfile));
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<User, UserDto>()
|
CreateMap<User, UserDto>()
|
||||||
.AfterMap<UserToUserDtoMappingAction>();
|
.AfterMap<UserToUserDtoMappingAction>();
|
||||||
|
|
||||||
|
CreateMap<UserProfile, UserProfileDto>()
|
||||||
|
.AfterMap<UserProfileToUserProfileDtoMappingAction>();
|
||||||
|
|
||||||
CreateMap<Friendship, FriendshipDto>();
|
CreateMap<Friendship, FriendshipDto>();
|
||||||
|
|
||||||
CreateMap<UserSession, SessionDto>();
|
CreateMap<UserSession, SessionDto>();
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Govor.Application.Interfaces.UserOnlineStatus;
|
||||||
|
using Govor.Application.Profiles;
|
||||||
|
using Govor.Contracts.DTOs;
|
||||||
|
using Govor.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace Govor.API.Common.Mapping;
|
||||||
|
|
||||||
|
public class UserProfileToUserProfileDtoMappingAction : IMappingAction<UserProfile, UserProfileDto>
|
||||||
|
{
|
||||||
|
private readonly IOnlineUserStore _onlineUserStore;
|
||||||
|
|
||||||
|
public UserProfileToUserProfileDtoMappingAction(IOnlineUserStore onlineUserStore)
|
||||||
|
{
|
||||||
|
_onlineUserStore = onlineUserStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Process(UserProfile source, UserProfileDto destination, ResolutionContext context)
|
||||||
|
{
|
||||||
|
destination.IsOnline = _onlineUserStore.IsOnline(source.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ namespace Govor.API.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(Roles = "Admin, User")]
|
[Authorize(Roles = "Admin, User")]
|
||||||
[Route("api/chats")]
|
[Route("api")]
|
||||||
public class ChatLoadController : Controller
|
public class ChatLoadController : Controller
|
||||||
{
|
{
|
||||||
private readonly ICurrentUserService _currentUser;
|
private readonly ICurrentUserService _currentUser;
|
||||||
@@ -31,9 +31,9 @@ public class ChatLoadController : Controller
|
|||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("group-messages")]
|
[HttpGet("groups/{groupId:guid}/messages")]
|
||||||
public async Task<IActionResult> GetGroupMessages(
|
public async Task<IActionResult> GetGroupMessages(
|
||||||
Guid chatId,
|
Guid groupId,
|
||||||
[FromQuery] MessageQuery query)
|
[FromQuery] MessageQuery query)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -42,7 +42,7 @@ public class ChatLoadController : Controller
|
|||||||
return BadRequest("Values must be non-negative and total must not exceed 100.");
|
return BadRequest("Values must be non-negative and total must not exceed 100.");
|
||||||
|
|
||||||
var result = await _messagesLoader.LoadMessagesInChatGroup(
|
var result = await _messagesLoader.LoadMessagesInChatGroup(
|
||||||
chatId,
|
groupId,
|
||||||
_currentUser.GetCurrentUserId(),
|
_currentUser.GetCurrentUserId(),
|
||||||
query.StartMessageId,
|
query.StartMessageId,
|
||||||
query.Before,
|
query.Before,
|
||||||
@@ -74,7 +74,7 @@ public class ChatLoadController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("user-messages")]
|
[HttpGet("user/{userId:guid}/messages")]
|
||||||
public async Task<IActionResult> GetUserMessages(
|
public async Task<IActionResult> GetUserMessages(
|
||||||
Guid userId,
|
Guid userId,
|
||||||
[FromQuery] MessageQuery query)
|
[FromQuery] MessageQuery query)
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using Govor.Application.Interfaces;
|
||||||
|
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
||||||
|
using Govor.Core.Models;
|
||||||
|
using Govor.Core.Repositories.PrivateChats;
|
||||||
|
using Govor.Core.Repositories.Users;
|
||||||
|
using Govor.Data.Repositories.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Govor.API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Roles = "Admin, User")]
|
||||||
|
[Route("api")]
|
||||||
|
public class PrivateChatController : Controller
|
||||||
|
{
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly IUsersRepository _usersRepository;
|
||||||
|
private readonly IVerifyFriendship _verifyFriendship;
|
||||||
|
private readonly IPrivateChatsRepository _privateChats;
|
||||||
|
private readonly ILogger<ChatLoadController> _logger;
|
||||||
|
|
||||||
|
public PrivateChatController(
|
||||||
|
ICurrentUserService currentUser,
|
||||||
|
IUsersRepository usersRepository,
|
||||||
|
IVerifyFriendship verifyFriendship,
|
||||||
|
IPrivateChatsRepository privateChats,
|
||||||
|
ILogger<ChatLoadController> logger)
|
||||||
|
{
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_usersRepository = usersRepository;
|
||||||
|
_verifyFriendship = verifyFriendship;
|
||||||
|
_privateChats = privateChats;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("user/{friendId:guid}/private-chat")]
|
||||||
|
public async Task<IActionResult> GetChatByFriendId(Guid friendId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentId = _currentUser.GetCurrentUserId();
|
||||||
|
|
||||||
|
if (!await _usersRepository.ExistsByIdAsync(friendId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("User not exist {0}", friendId);
|
||||||
|
return NotFound("User not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _verifyFriendship.VerifyAsync(currentId, friendId);
|
||||||
|
|
||||||
|
|
||||||
|
var chatId = await GetPrivateChatsIdAsync(currentId, friendId);
|
||||||
|
return Ok(chatId);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex.Message);
|
||||||
|
return Forbid(ex.Message);
|
||||||
|
}
|
||||||
|
catch (NotFoundException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, ex.Message);
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, ex.Message);
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, ex.Message);
|
||||||
|
return StatusCode(500, "Unexpected Error! Please try again later.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Guid> GetPrivateChatsIdAsync(Guid userA, Guid userB)
|
||||||
|
{
|
||||||
|
Guid recipientId;
|
||||||
|
// Makes new PrivateChat if not exist
|
||||||
|
if (!_privateChats.Exist(userA, userB))
|
||||||
|
{
|
||||||
|
recipientId = Guid.NewGuid();
|
||||||
|
await _privateChats.AddAsync(new PrivateChat()
|
||||||
|
{ Id = recipientId, UserAId = userA, UserBId = userB });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
recipientId = (await _privateChats.GetByMembersAsync(userA, userB)).Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,14 +72,6 @@ public class ProfileController : ControllerBase
|
|||||||
await _profileService.SetNewIcon(userId, mediaInfo.MediaId);
|
await _profileService.SetNewIcon(userId, mediaInfo.MediaId);
|
||||||
var iconId = mediaInfo.MediaId;
|
var iconId = mediaInfo.MediaId;
|
||||||
|
|
||||||
var payload = new { userId, iconId };
|
|
||||||
|
|
||||||
await _profileHubContext.Clients.All.SendAsync(
|
|
||||||
"AvatarUpdated",
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return Ok(mediaInfo);
|
return Ok(mediaInfo);
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
|
|||||||
+94
-201
@@ -1,6 +1,6 @@
|
|||||||
using Govor.API.Common.SignalR.Helpers;
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
|
using Govor.API.Hubs.Infrastructure;
|
||||||
using Govor.Application.Exceptions.VerifyFriendship;
|
using Govor.Application.Exceptions.VerifyFriendship;
|
||||||
using Govor.Application.Interfaces;
|
|
||||||
using Govor.Application.Interfaces.Messages;
|
using Govor.Application.Interfaces.Messages;
|
||||||
using Govor.Application.Interfaces.Messages.Parameters;
|
using Govor.Application.Interfaces.Messages.Parameters;
|
||||||
using Govor.Contracts.Requests.SignalR;
|
using Govor.Contracts.Requests.SignalR;
|
||||||
@@ -15,20 +15,23 @@ namespace Govor.API.Hubs;
|
|||||||
public class ChatsHub : Hub
|
public class ChatsHub : Hub
|
||||||
{
|
{
|
||||||
private readonly ILogger<ChatsHub> _logger;
|
private readonly ILogger<ChatsHub> _logger;
|
||||||
private readonly IMessageCommandService _messageCommandService;
|
private readonly IMessageCommandService _commandService;
|
||||||
private readonly IUserGroupsService _userService;
|
|
||||||
private readonly IHubUserAccessor _userAccessor;
|
private readonly IHubUserAccessor _userAccessor;
|
||||||
|
private readonly IChatNotificationService _notifier;
|
||||||
|
private readonly IConnectionManager _connectionManager;
|
||||||
|
|
||||||
public ChatsHub(
|
public ChatsHub(
|
||||||
ILogger<ChatsHub> logger,
|
ILogger<ChatsHub> logger,
|
||||||
IMessageCommandService messageCommandService,
|
IMessageCommandService commandService,
|
||||||
IUserGroupsService userService,
|
IHubUserAccessor userAccessor,
|
||||||
IHubUserAccessor userAccessor)
|
IChatNotificationService notifier,
|
||||||
|
IConnectionManager connectionManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_messageCommandService = messageCommandService;
|
_commandService = commandService;
|
||||||
_userService = userService;
|
|
||||||
_userAccessor = userAccessor;
|
_userAccessor = userAccessor;
|
||||||
|
_notifier = notifier;
|
||||||
|
_connectionManager = connectionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
@@ -36,132 +39,58 @@ public class ChatsHub : Hub
|
|||||||
var userId = _userAccessor.GetUserId(Context);
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
if (userId == Guid.Empty)
|
if (userId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User connected with invalid UserID claim.");
|
|
||||||
Context.Abort();
|
Context.Abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
|
await _connectionManager.OnConnectedAsync(Context.ConnectionId, userId);
|
||||||
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group",
|
_logger.LogInformation("User {UserId} connected", userId);
|
||||||
userId, Context.ConnectionId);
|
|
||||||
|
|
||||||
var userGroups = await _userService.GetUserGroupsAsync(userId);
|
|
||||||
|
|
||||||
foreach (var group in userGroups)
|
|
||||||
{
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"group_{group.Id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnConnectedAsync();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
var userId = _userAccessor.GetUserId(Context, true);
|
var userId = _userAccessor.GetUserId(Context, true);
|
||||||
if (userId != Guid.Empty)
|
await _connectionManager.OnDisconnectedAsync(Context.ConnectionId, userId);
|
||||||
{
|
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
|
|
||||||
_logger.LogInformation("User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group",
|
|
||||||
userId, Context.ConnectionId);
|
|
||||||
|
|
||||||
var userGroups = await _userService.GetUserGroupsAsync(userId);
|
if (exception != null)
|
||||||
|
_logger.LogWarning(exception, "User {UserId} disconnected with error", userId);
|
||||||
foreach (var group in userGroups)
|
|
||||||
{
|
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group_{group.Id}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (exception != null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(exception,
|
|
||||||
"User disconnected with an exception and invalid UserID claim. ConnectionId: {ConnectionId}",
|
|
||||||
Context.ConnectionId);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
_logger.LogInformation("User {UserId} disconnected", userId);
|
||||||
_logger.LogInformation(
|
|
||||||
"User disconnected with no exception and invalid UserID claim. ConnectionId: {ConnectionId}",
|
|
||||||
Context.ConnectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnDisconnectedAsync(exception);
|
await base.OnDisconnectedAsync(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SEND ---
|
||||||
public async Task<HubResult<UserMessageResponse>> Send(MessageRequest request)
|
public async Task<HubResult<UserMessageResponse>> Send(MessageRequest request)
|
||||||
{
|
{
|
||||||
var senderId = _userAccessor.GetUserId(Context);
|
return await SafeExecute(async (userId) =>
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
|
|
||||||
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Empty message received from user {UserId}", senderId);
|
ValidateMessageRequest(request);
|
||||||
return HubResult<UserMessageResponse>.BadRequest("Message must contain content or media.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.EncryptedContent.Length > 50_000)
|
var sendParams = MapToSendMessage(request, userId);
|
||||||
{
|
var result = await _commandService.SendMessageAsync(sendParams);
|
||||||
_logger.LogWarning("User {SenderId} tried to send a too long message (length: {Length})", senderId, request.EncryptedContent.Length);
|
|
||||||
return HubResult<UserMessageResponse>.BadRequest("Message cannot exceed 50,000 characters.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Sending message from {SenderId} to {RecipientId} ({RecipientType})",
|
if (!result.IsSuccess)
|
||||||
senderId, request.RecipientId, request.RecipientType);
|
throw new InvalidOperationException(result.Exception.Message ?? "Failed to send message");
|
||||||
|
|
||||||
var sendMessageParams = new SendMessage(
|
var response = MapToResponse(result.Message, request.ReplyToMessageId);
|
||||||
EncryptContent: request.EncryptedContent,
|
|
||||||
ReplyToMessageId: request.ReplyToMessageId,
|
|
||||||
FromUserId: senderId,
|
|
||||||
RecipientId: request.RecipientId,
|
|
||||||
RecipientType: request.RecipientType,
|
|
||||||
SendAt: DateTime.UtcNow,
|
|
||||||
Media: request.MediaAttachments?.Select(f => new SendMedia(f.MediaId, f.EncryptedKey)) ??
|
|
||||||
Array.Empty<SendMedia>()
|
|
||||||
);
|
|
||||||
|
|
||||||
try
|
await _notifier.NotifyMessageSentAsync(response);
|
||||||
{
|
|
||||||
var result = await _messageCommandService.SendMessageAsync(sendMessageParams);
|
|
||||||
|
|
||||||
if (!result.IsSuccess || result.Message.Id == Guid.Empty)
|
return HubResult<UserMessageResponse>.Ok(response);
|
||||||
return LogAndError<UserMessageResponse>(senderId, request.RecipientId, "Failed to send message", result.Exception);
|
}, request.RecipientId);
|
||||||
|
|
||||||
var response = BuildUserMessageResponse(result.Message, request.ReplyToMessageId);
|
|
||||||
|
|
||||||
await NotifyClientsAboutMessage(response);
|
|
||||||
|
|
||||||
return HubResult<UserMessageResponse>.Ok(response);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
return LogAndUnauthorized<UserMessageResponse>(ex, "Unauthorized sending attempt", senderId,
|
|
||||||
request.RecipientId);
|
|
||||||
}
|
|
||||||
catch (FriendshipException)
|
|
||||||
{
|
|
||||||
return HubResult<UserMessageResponse>.Unauthorized(
|
|
||||||
"You cannot send this message because you are not friends.");
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
return HubResult<UserMessageResponse>.BadRequest("Invalid message content.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return LogAndError<UserMessageResponse>(senderId, request.RecipientId, "Unhandled exception", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- REMOVE ---
|
||||||
public async Task<HubResult<MessageRemovedResponse>> Remove(RemoveMessageRequest request)
|
public async Task<HubResult<MessageRemovedResponse>> Remove(RemoveMessageRequest request)
|
||||||
{
|
{
|
||||||
var removerId = _userAccessor.GetUserId(Context);
|
return await SafeExecute(async (userId) =>
|
||||||
_logger.LogInformation("Removing message {MessageId} by user {RemoverId}", request.MessageId, removerId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var result = await _messageCommandService.DeleteMessageAsync(new DeleteMessage(removerId, request.MessageId));
|
var result = await _commandService.DeleteMessageAsync(new DeleteMessage(userId, request.MessageId));
|
||||||
|
|
||||||
if (!result.IsSuccess || result.OriginalMessage == null)
|
if (!result.IsSuccess || result.OriginalMessage == null)
|
||||||
return LogAndError<MessageRemovedResponse>(removerId, request.MessageId, "Message deletion failed", result.Exception);
|
throw new InvalidOperationException("Message deletion failed");
|
||||||
|
|
||||||
var notification = new MessageRemovedResponse
|
var notification = new MessageRemovedResponse
|
||||||
{
|
{
|
||||||
@@ -171,76 +100,98 @@ public class ChatsHub : Hub
|
|||||||
RecipientType = result.OriginalMessage.RecipientType
|
RecipientType = result.OriginalMessage.RecipientType
|
||||||
};
|
};
|
||||||
|
|
||||||
await NotifyClientsAboutRemoval(notification);
|
await _notifier.NotifyMessageRemovedAsync(notification);
|
||||||
|
|
||||||
_logger.LogInformation("Message {MessageId} removed successfully by {RemoverId}", request.MessageId, removerId);
|
|
||||||
return HubResult<MessageRemovedResponse>.Ok(notification);
|
return HubResult<MessageRemovedResponse>.Ok(notification);
|
||||||
}
|
}, request.MessageId);
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
return LogAndUnauthorized<MessageRemovedResponse>(ex, "Unauthorized removal", removerId, request.MessageId);
|
|
||||||
}
|
|
||||||
catch (KeyNotFoundException ex)
|
|
||||||
{
|
|
||||||
return LogAndNotFound<MessageRemovedResponse>(ex, "Message not found", removerId, request.MessageId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return LogAndError<MessageRemovedResponse>(removerId, request.MessageId, "Unhandled deletion error", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- EDIT ---
|
||||||
public async Task<HubResult<MessageEditResponse>> Edit(EditMessageRequest request)
|
public async Task<HubResult<MessageEditResponse>> Edit(EditMessageRequest request)
|
||||||
{
|
{
|
||||||
var editor = _userAccessor.GetUserId(Context);
|
return await SafeExecute(async (userId) =>
|
||||||
_logger.LogInformation("Editing message {MessageId} by user {EditorId}", request.MessageId, editor);
|
|
||||||
|
|
||||||
var editMessageParam = new EditMessage(editor,
|
|
||||||
request.MessageId,
|
|
||||||
request.NewEncryptedContent,
|
|
||||||
DateTime.UtcNow);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var result = await _messageCommandService.EditMessageAsync(editMessageParam);
|
var editParams = new EditMessage(userId, request.MessageId, request.NewEncryptedContent, DateTime.UtcNow);
|
||||||
|
var result = await _commandService.EditMessageAsync(editParams);
|
||||||
|
|
||||||
if (!result.IsSuccess || result.OriginalMessage == null)
|
if (!result.IsSuccess || result.OriginalMessage == null)
|
||||||
return LogAndError<MessageEditResponse>(editor, request.MessageId, "Edit message error",
|
throw new InvalidOperationException("Edit message error");
|
||||||
result.Exception);
|
|
||||||
|
|
||||||
var response = new MessageEditResponse()
|
var response = new MessageEditResponse
|
||||||
{
|
{
|
||||||
MessageId = result.messageId,
|
MessageId = result.messageId,
|
||||||
EditorId = editor,
|
EditorId = userId,
|
||||||
RecipientId = result.OriginalMessage.RecipientId,
|
RecipientId = result.OriginalMessage.RecipientId,
|
||||||
RecipientType = result.OriginalMessage.RecipientType,
|
RecipientType = result.OriginalMessage.RecipientType,
|
||||||
NewEncryptedContent = request.NewEncryptedContent,
|
NewEncryptedContent = request.NewEncryptedContent,
|
||||||
EditedAt = editMessageParam.EditedAt,
|
EditedAt = editParams.EditedAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
await NotifyClientsAboutEdit(response);
|
await _notifier.NotifyMessageEditedAsync(response);
|
||||||
|
|
||||||
_logger.LogInformation("Message {MessageId} edited successfully by {editor}", request.MessageId, editor);
|
|
||||||
|
|
||||||
return HubResult<MessageEditResponse>.Ok(response);
|
return HubResult<MessageEditResponse>.Ok(response);
|
||||||
|
}, request.MessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HubResult<T>> SafeExecute<T>(Func<Guid, Task<HubResult<T>>> action, Guid targetIdForLog)
|
||||||
|
{
|
||||||
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await action(userId);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
return LogAndUnauthorized<MessageEditResponse>(ex, "Unauthorized editing", editor, request.MessageId);
|
_logger.LogWarning(ex, "Unauthorized: {UserId} -> {TargetId}", userId, targetIdForLog);
|
||||||
|
return HubResult<T>.Unauthorized("You are not authorized.");
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException ex)
|
catch (FriendshipException)
|
||||||
{
|
{
|
||||||
return LogAndNotFound<MessageEditResponse>(ex, "Message not found", editor, request.MessageId);
|
return HubResult<T>.Unauthorized("You cannot perform this action due to friendship status.");
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
return HubResult<T>.BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
return HubResult<T>.NotFound("Resource not found.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return LogAndError<MessageEditResponse>(editor, request.MessageId, "Unhandled exception error", ex);
|
_logger.LogError(ex, "Error executing hub method for {UserId}", userId);
|
||||||
|
return HubResult<T>.Error("Internal server error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ValidateMessageRequest(MessageRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
|
||||||
|
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Message must contain content or media.");
|
||||||
|
}
|
||||||
|
if (request.EncryptedContent.Length > 50_000)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Message is too long.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region common
|
private SendMessage MapToSendMessage(MessageRequest request, Guid senderId)
|
||||||
private UserMessageResponse BuildUserMessageResponse(Message message, Guid? replyToId)
|
{
|
||||||
|
return new SendMessage(
|
||||||
|
EncryptContent: request.EncryptedContent,
|
||||||
|
ReplyToMessageId: request.ReplyToMessageId,
|
||||||
|
FromUserId: senderId,
|
||||||
|
RecipientId: request.RecipientId,
|
||||||
|
RecipientType: request.RecipientType,
|
||||||
|
SendAt: DateTime.UtcNow,
|
||||||
|
Media: request.MediaAttachments?.Select(f => new SendMedia(f.MediaId, f.EncryptedKey))
|
||||||
|
?? Array.Empty<SendMedia>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserMessageResponse MapToResponse(Message message, Guid? replyToId)
|
||||||
{
|
{
|
||||||
return new UserMessageResponse
|
return new UserMessageResponse
|
||||||
{
|
{
|
||||||
@@ -255,62 +206,4 @@ public class ChatsHub : Hub
|
|||||||
ReplyToMessageId = replyToId
|
ReplyToMessageId = replyToId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task NotifyClientsAboutMessage(UserMessageResponse response)
|
|
||||||
{
|
|
||||||
string group = response.RecipientType == RecipientType.User
|
|
||||||
? response.RecipientId.ToString()
|
|
||||||
: $"group_{response.RecipientId}";
|
|
||||||
|
|
||||||
await Clients.Group(group).SendAsync("ReceiveMessage", response);
|
|
||||||
await Clients.Caller.SendAsync("MessageSent", response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NotifyClientsAboutRemoval(MessageRemovedResponse response)
|
|
||||||
{
|
|
||||||
if (response.RecipientType == RecipientType.User)
|
|
||||||
{
|
|
||||||
await Clients.Group(response.SenderId.ToString()).SendAsync("MessageRemoved", response);
|
|
||||||
if (response.SenderId != response.RecipientId)
|
|
||||||
await Clients.Group(response.RecipientId.ToString()).SendAsync("MessageRemoved", response);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Clients.Group($"group_{response.RecipientId}").SendAsync("MessageRemoved", response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NotifyClientsAboutEdit(MessageEditResponse response)
|
|
||||||
{
|
|
||||||
if (response.RecipientType == RecipientType.User)
|
|
||||||
{
|
|
||||||
await Clients.Group(response.EditorId.ToString()).SendAsync("MessageEdit", response);
|
|
||||||
if (response.EditorId != response.RecipientId)
|
|
||||||
await Clients.Group(response.RecipientId.ToString()).SendAsync("MessageEdit", response);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Clients.Group($"group_{response.RecipientId}").SendAsync("MessageEdit", response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logging helpers
|
|
||||||
private HubResult<T> LogAndError<T>(Guid userId, Guid targetId, string message, Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "{Message} from {UserId} to {TargetId}", message, userId, targetId);
|
|
||||||
return HubResult<T>.Error(ex?.Message ?? "Internal server error");
|
|
||||||
}
|
|
||||||
|
|
||||||
private HubResult<T> LogAndUnauthorized<T>(Exception ex, string msg, Guid userId, Guid targetId)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
|
|
||||||
return HubResult<T>.Unauthorized("You are not authorized to perform this action.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private HubResult<T> LogAndNotFound<T>(Exception ex, string msg, Guid userId, Guid targetId)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
|
|
||||||
return HubResult<T>.NotFound("Message not found.");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,7 @@ public class FriendsHub : Hub
|
|||||||
|
|
||||||
await base.OnConnectedAsync();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
var userId = _userAccessor.GetUserId(Context, true);
|
var userId = _userAccessor.GetUserId(Context, true);
|
||||||
@@ -76,9 +77,13 @@ public class FriendsHub : Hub
|
|||||||
{
|
{
|
||||||
var userId = _userAccessor.GetUserId(Context);
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
var friendship = await _friendRequestService.SendAsync(userId, targetUserId);
|
var friendship = await _friendRequestService.SendAsync(userId, targetUserId);
|
||||||
|
var dto = _mapper.Map<FriendshipDto>(friendship);
|
||||||
|
|
||||||
await Clients.Group(targetUserId.ToString())
|
await Clients.Group(targetUserId.ToString())
|
||||||
.SendAsync("FriendRequestReceived", _mapper.Map<FriendshipDto>(friendship));
|
.SendAsync("FriendRequestReceived", dto);
|
||||||
|
|
||||||
|
await Clients.Group(userId.ToString())
|
||||||
|
.SendAsync("YourFriendRequestReceived", dto);
|
||||||
|
|
||||||
_logger.LogInformation($"Friend request received for user {targetUserId} from {userId}.");
|
_logger.LogInformation($"Friend request received for user {targetUserId} from {userId}.");
|
||||||
return HubResult<object>.Created();
|
return HubResult<object>.Created();
|
||||||
@@ -111,12 +116,13 @@ public class FriendsHub : Hub
|
|||||||
{
|
{
|
||||||
var userId = _userAccessor.GetUserId(Context);
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
var friendship = await _friendRequestService.AcceptAsync(friendshipId, userId);
|
var friendship = await _friendRequestService.AcceptAsync(friendshipId, userId);
|
||||||
|
var dto = _mapper.Map<FriendshipDto>(friendship);
|
||||||
|
|
||||||
await Clients.Group(userId.ToString())
|
await Clients.Group(userId.ToString())
|
||||||
.SendAsync("FriendRequestAccepted", _mapper.Map<FriendshipDto>(friendship));
|
.SendAsync("FriendRequestAccepted", dto);
|
||||||
|
|
||||||
await Clients.Group(friendship.RequesterId.ToString())
|
await Clients.Group(friendship.RequesterId.ToString())
|
||||||
.SendAsync( "YourFriendRequestAccepted", _mapper.Map<UserDto>(friendship.Addressee));
|
.SendAsync( "YourFriendRequestAccepted", dto);
|
||||||
|
|
||||||
_logger.LogInformation($"Friend request accepted for user {userId} from {userId}.");
|
_logger.LogInformation($"Friend request accepted for user {userId} from {userId}.");
|
||||||
return HubResult<object>.Ok();
|
return HubResult<object>.Ok();
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Govor.API.Hubs.Infrastructure;
|
||||||
|
|
||||||
|
public static class ChatHubConstants
|
||||||
|
{
|
||||||
|
public const string ReceiveMessage = "ReceiveMessage";
|
||||||
|
public const string MessageSent = "MessageSent";
|
||||||
|
public const string MessageRemoved = "MessageRemoved";
|
||||||
|
public const string MessageEdited = "MessageEdit";
|
||||||
|
|
||||||
|
public static string GetUserGroup(Guid userId) => userId.ToString();
|
||||||
|
public static string GetChatGroup(Guid groupId) => $"group_{groupId}";
|
||||||
|
public static string GetPrivateChat(Guid groupId) => $"private_{groupId}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Govor.Contracts.Responses.SignalR;
|
||||||
|
using Govor.Core.Models.Messages;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Govor.API.Hubs.Infrastructure;
|
||||||
|
|
||||||
|
public class ChatNotificationService : IChatNotificationService
|
||||||
|
{
|
||||||
|
private readonly IHubContext<ChatsHub> _hubContext;
|
||||||
|
|
||||||
|
public ChatNotificationService(IHubContext<ChatsHub> hubContext)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NotifyMessageSentAsync(UserMessageResponse message)
|
||||||
|
{
|
||||||
|
if (message.RecipientType == RecipientType.User)
|
||||||
|
{
|
||||||
|
await _hubContext.Clients.Group(ChatHubConstants.GetPrivateChat(message.RecipientId))
|
||||||
|
.SendAsync(ChatHubConstants.ReceiveMessage, message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _hubContext.Clients.Group(ChatHubConstants.GetChatGroup(message.RecipientId))
|
||||||
|
.SendAsync(ChatHubConstants.ReceiveMessage, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// await _hubContext.Clients.Group(ChatHubConstants.GetUserGroup(message.SenderId))
|
||||||
|
// .SendAsync(ChatHubConstants.MessageSent, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NotifyMessageRemovedAsync(MessageRemovedResponse response)
|
||||||
|
{
|
||||||
|
await NotifyParticipantsAsync(
|
||||||
|
response.SenderId,
|
||||||
|
response.RecipientId,
|
||||||
|
response.RecipientType,
|
||||||
|
ChatHubConstants.MessageRemoved,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NotifyMessageEditedAsync(MessageEditResponse response)
|
||||||
|
{
|
||||||
|
await NotifyParticipantsAsync(
|
||||||
|
response.EditorId,
|
||||||
|
response.RecipientId,
|
||||||
|
response.RecipientType,
|
||||||
|
ChatHubConstants.MessageEdited,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyParticipantsAsync(Guid initiatorId, Guid targetId, RecipientType type, string method, object payload)
|
||||||
|
{
|
||||||
|
if (type == RecipientType.User)
|
||||||
|
{
|
||||||
|
await _hubContext.Clients.Group(ChatHubConstants.GetUserGroup(initiatorId))
|
||||||
|
.SendAsync(method, payload);
|
||||||
|
|
||||||
|
if (initiatorId != targetId)
|
||||||
|
{
|
||||||
|
await _hubContext.Clients.Group(ChatHubConstants.GetUserGroup(targetId))
|
||||||
|
.SendAsync(method, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// to groups
|
||||||
|
await _hubContext.Clients.Group(ChatHubConstants.GetChatGroup(targetId))
|
||||||
|
.SendAsync(method, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Govor.Application.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Govor.API.Hubs.Infrastructure;
|
||||||
|
|
||||||
|
public class ConnectionManager : IConnectionManager
|
||||||
|
{
|
||||||
|
private readonly IUserGroupsGetterService _userGroupsGetterService;
|
||||||
|
private readonly IUserPrivateChatsGetterService _userPrivateChatsGetterService;
|
||||||
|
private readonly IHubContext<ChatsHub> _hubContext;
|
||||||
|
|
||||||
|
public ConnectionManager(IUserGroupsGetterService userGroupsGetterService, IUserPrivateChatsGetterService userPrivateChatsGetterService, IHubContext<ChatsHub> hubContext)
|
||||||
|
{
|
||||||
|
_userGroupsGetterService = userGroupsGetterService;
|
||||||
|
_userPrivateChatsGetterService = userPrivateChatsGetterService;
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnConnectedAsync(string connectionId, Guid userId)
|
||||||
|
{
|
||||||
|
// user
|
||||||
|
await _hubContext.Groups.AddToGroupAsync(connectionId, ChatHubConstants.GetUserGroup(userId));
|
||||||
|
|
||||||
|
// groups
|
||||||
|
var userGroups = await _userGroupsGetterService.GetUserGroupsAsync(userId);
|
||||||
|
foreach (var group in userGroups)
|
||||||
|
{
|
||||||
|
await _hubContext.Groups.AddToGroupAsync(connectionId, ChatHubConstants.GetChatGroup(group.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// private chats
|
||||||
|
var chats = await _userPrivateChatsGetterService.GetUserChatsAsync(userId);
|
||||||
|
foreach (var group in chats)
|
||||||
|
{
|
||||||
|
await _hubContext.Groups.AddToGroupAsync(connectionId, ChatHubConstants.GetPrivateChat(group.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnDisconnectedAsync(string connectionId, Guid userId)
|
||||||
|
{
|
||||||
|
if (userId != Guid.Empty)
|
||||||
|
{
|
||||||
|
await _hubContext.Groups.RemoveFromGroupAsync(connectionId, ChatHubConstants.GetUserGroup(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Govor.Contracts.Responses.SignalR;
|
||||||
|
|
||||||
|
namespace Govor.API.Hubs.Infrastructure;
|
||||||
|
|
||||||
|
public interface IChatNotificationService
|
||||||
|
{
|
||||||
|
Task NotifyMessageSentAsync(UserMessageResponse message);
|
||||||
|
Task NotifyMessageRemovedAsync(MessageRemovedResponse response);
|
||||||
|
Task NotifyMessageEditedAsync(MessageEditResponse response);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Govor.API.Hubs.Infrastructure;
|
||||||
|
|
||||||
|
public interface IConnectionManager
|
||||||
|
{
|
||||||
|
Task OnConnectedAsync(string connectionId, Guid userId);
|
||||||
|
Task OnDisconnectedAsync(string connectionId, Guid userId);
|
||||||
|
}
|
||||||
@@ -34,21 +34,22 @@ public class PresenceHub : Hub
|
|||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
var userId = _userAccessor.GetUserId(Context);
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
if (userId == Guid.Empty || await _users.ExistsByIdAsync(userId) == false)
|
|
||||||
|
if (userId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User connected with invalid UserId claim.");
|
_logger.LogWarning("User connected with invalid UserId claim.");
|
||||||
Context.Abort();
|
Context.Abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onlineUserStore.SetOnlineUser(userId);
|
var isFirstConnection = _onlineUserStore.AddConnection(userId, Context.ConnectionId);
|
||||||
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
|
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
|
||||||
|
|
||||||
var friends = await _scopeService.GetNotifiedUsers(userId);
|
if (isFirstConnection)
|
||||||
|
|
||||||
foreach (var recipient in friends)
|
|
||||||
{
|
{
|
||||||
await Clients.Group(recipient.ToString())
|
var friends = await _scopeService.GetNotifiedUsers(userId);
|
||||||
|
await Clients.Groups(friends.Select(f => f.ToString()).ToList())
|
||||||
.SendAsync("UserOnline", userId);
|
.SendAsync("UserOnline", userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,19 +75,36 @@ public class PresenceHub : Hub
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onlineUserStore.SetOfflineUser(userId);
|
var isLastConnection = _onlineUserStore.RemoveConnection(userId, Context.ConnectionId);
|
||||||
|
|
||||||
// Updating was online
|
if (isLastConnection)
|
||||||
var user = await _users.FindByIdAsync(userId);
|
|
||||||
user.WasOnline = DateTime.UtcNow;
|
|
||||||
await _users.UpdateAsync(user);
|
|
||||||
|
|
||||||
var friends = await _scopeService.GetNotifiedUsers(userId);
|
|
||||||
|
|
||||||
foreach (var recipient in friends)
|
|
||||||
{
|
{
|
||||||
await Clients.Group(recipient.ToString())
|
_ = Task.Run(async () =>
|
||||||
.SendAsync("UserOffline", userId);
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
var currentConnections = _onlineUserStore.GetConnections(userId);
|
||||||
|
if (currentConnections == null || !currentConnections.Any())
|
||||||
|
{
|
||||||
|
var friends = await _scopeService.GetNotifiedUsers(userId);
|
||||||
|
await Clients.Groups(friends.Select(f => f.ToString()).ToList())
|
||||||
|
.SendAsync("UserOffline", userId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await _users.FindByIdAsync(userId);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.WasOnline = DateTime.UtcNow;
|
||||||
|
await _users.UpdateAsync(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error updating WasOnline for {UserId}", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.OnDisconnectedAsync(exception);
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
|||||||
+80
-104
@@ -1,11 +1,9 @@
|
|||||||
using Govor.API.Common.SignalR.Helpers;
|
using Govor.API.Common.SignalR.Helpers;
|
||||||
using Govor.Application.Interfaces;
|
using Govor.Application.Interfaces;
|
||||||
using Govor.Application.Interfaces.Friends;
|
using Govor.Application.Interfaces.Friends;
|
||||||
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
|
||||||
using Govor.Application.Interfaces.Medias;
|
using Govor.Application.Interfaces.Medias;
|
||||||
|
using Govor.Contracts.DTOs;
|
||||||
using Govor.Contracts.Responses.SignalR;
|
using Govor.Contracts.Responses.SignalR;
|
||||||
using Govor.Core.Models;
|
|
||||||
using Govor.Core.Models.Users;
|
|
||||||
using Govor.Core.Repositories.Groups;
|
using Govor.Core.Repositories.Groups;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
@@ -19,6 +17,7 @@ public class ProfileHub : Hub
|
|||||||
private readonly IFriendshipService _friendsService;
|
private readonly IFriendshipService _friendsService;
|
||||||
private readonly IProfileService _profileService;
|
private readonly IProfileService _profileService;
|
||||||
private readonly IHubUserAccessor _userAccessor;
|
private readonly IHubUserAccessor _userAccessor;
|
||||||
|
private readonly ISynchingService _synchingService;
|
||||||
private readonly ILogger<ProfileHub> _logger;
|
private readonly ILogger<ProfileHub> _logger;
|
||||||
private readonly IMediaService _mediaService;
|
private readonly IMediaService _mediaService;
|
||||||
|
|
||||||
@@ -27,12 +26,16 @@ public class ProfileHub : Hub
|
|||||||
IFriendshipService friendsService,
|
IFriendshipService friendsService,
|
||||||
IProfileService profileService,
|
IProfileService profileService,
|
||||||
IHubUserAccessor userAccessor,
|
IHubUserAccessor userAccessor,
|
||||||
|
ISynchingService synchingService,
|
||||||
|
IMediaService mediaService,
|
||||||
ILogger<ProfileHub> logger)
|
ILogger<ProfileHub> logger)
|
||||||
{
|
{
|
||||||
_groupsRepository = groupsRepository;
|
_groupsRepository = groupsRepository;
|
||||||
_friendsService = friendsService;
|
_friendsService = friendsService;
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
_userAccessor = userAccessor;
|
_userAccessor = userAccessor;
|
||||||
|
_synchingService = synchingService;
|
||||||
|
_mediaService = mediaService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,33 +43,7 @@ public class ProfileHub : Hub
|
|||||||
{
|
{
|
||||||
var userId = _userAccessor.GetUserId(Context);
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{userId}");
|
await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
|
||||||
|
|
||||||
// Friends
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var friendships = await _friendsService.GetFriendsAsync(userId);
|
|
||||||
foreach (var friends in friendships)
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"friends-{friends.Id}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to get friends for user {userId}", userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Groups
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var groups = await _groupsRepository.GetByUserIdAsync(userId);
|
|
||||||
foreach (var group in groups)
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"group-{group.Id}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to get groups for user {userId}", userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId);
|
|
||||||
|
|
||||||
await base.OnConnectedAsync();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
@@ -77,56 +54,35 @@ public class ProfileHub : Hub
|
|||||||
|
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{userId}");
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{userId}");
|
||||||
|
|
||||||
// Friends
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var friendships = await _friendsService.GetFriendsAsync(userId);
|
|
||||||
|
|
||||||
foreach (var friendship in friendships)
|
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"friends-{friendship.Id}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to remove user {userId} from friend groups", userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Groups
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var groups = await _groupsRepository.GetByUserIdAsync(userId);
|
|
||||||
|
|
||||||
foreach (var group in groups)
|
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group-{group.Id}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to remove user {userId} from groups", userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnDisconnectedAsync(exception);
|
await base.OnDisconnectedAsync(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HubResult<bool>> SetDescription(string description)
|
public async Task<HubResult<bool>> SetDescription(string description)
|
||||||
{
|
{
|
||||||
if(description.Length > 500)
|
if (description.Length > 500)
|
||||||
return HubResult<bool>.Error("The description cannot be longer than 500 characters.");
|
return HubResult<bool>.Error("Description length exceeded.");
|
||||||
|
|
||||||
var userId = _userAccessor.GetUserId(Context);
|
var userId = _userAccessor.GetUserId(Context);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
description = _synchingService.NormalizeNewlines(description);
|
||||||
await _profileService.SetDescription(description, userId);
|
await _profileService.SetDescription(description, userId);
|
||||||
|
|
||||||
var payload = new { userId, description };
|
var payload = new DescriptionUpdatePayload
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Description = description
|
||||||
|
};
|
||||||
|
|
||||||
await NotifyFriendsAndGroupsAsync(userId, "DescriptionUpdated", payload);
|
await NotifyProfileUpdatedAsync(userId, "DescriptionUpdated", payload);
|
||||||
|
|
||||||
return HubResult<bool>.Ok(true);
|
return HubResult<bool>.Ok(true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "An error occurred while updating the user's {userId} descripton!", userId);
|
_logger.LogError(ex, "Failed to update description for user {UserId}", userId);
|
||||||
return HubResult<bool>.Error("An unaccounted error on the server!");
|
return HubResult<bool>.Error("Server error.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,14 +92,14 @@ public class ProfileHub : Hub
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (iconId == Guid.Empty)
|
if (iconId == Guid.Empty || !await _mediaService.HasMediaAsync(iconId))
|
||||||
return HubResult<bool>.BadRequest("IconId can't be empty!");
|
return HubResult<bool>.BadRequest("Invalid icon id.");
|
||||||
|
|
||||||
await _profileService.SetNewIcon(userId, iconId);
|
await _profileService.SetNewIcon(userId, iconId);
|
||||||
|
|
||||||
var payload = new { userId, iconId };
|
var payload = new AvatarUpdatePayload(){UserId = userId, IconId = iconId};
|
||||||
|
|
||||||
await NotifyFriendsAndGroupsAsync(userId, "AvatarUpdated", payload);
|
await NotifyProfileUpdatedAsync(userId, "AvatarUpdated", payload);
|
||||||
|
|
||||||
return HubResult<bool>.Ok(true);
|
return HubResult<bool>.Ok(true);
|
||||||
}
|
}
|
||||||
@@ -154,50 +110,70 @@ public class ProfileHub : Hub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task NotifyFriendsAndGroupsAsync(Guid userId, string eventName, object payload)
|
private async Task NotifyProfileUpdatedAsync(
|
||||||
|
Guid UserId,
|
||||||
|
string eventName,
|
||||||
|
object payload)
|
||||||
{
|
{
|
||||||
var friendIds = Enumerable.Empty<User>();
|
|
||||||
var groups = Enumerable.Empty<ChatGroup>();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
friendIds = await _friendsService.GetFriendsAsync(userId);
|
var recipients = new HashSet<Guid>();
|
||||||
|
|
||||||
|
// 1. Friends
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var friends = await _friendsService.GetFriendsAsync(UserId);
|
||||||
|
recipients.UnionWith(friends.Select(f => f.Id));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "User has no friends for user {userId}", UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. (pending)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var potentialFriends = await _friendsService.GetPotentialFriendsAsync(UserId);
|
||||||
|
recipients.UnionWith(potentialFriends.Select(p => p.Id));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "User has potential friends for user {userId}", UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3. groups
|
||||||
|
/*var groups = await _groupsRepository.GetByUserIdAsync(authorUserId);
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var members =
|
||||||
|
await _groupsRepository.Get(group.Id);
|
||||||
|
|
||||||
|
recipients.UnionWith(members.Select(m => m.Id));
|
||||||
|
}*/
|
||||||
|
|
||||||
|
recipients.Add(UserId);
|
||||||
|
|
||||||
|
// 5.
|
||||||
|
// recipients.RemoveWhere(id =>
|
||||||
|
// !_profileVisibilityService.CanViewProfile(id, authorUserId));
|
||||||
|
|
||||||
|
|
||||||
|
// 6. 1 user = 1 event
|
||||||
|
var recipientsStrings =
|
||||||
|
recipients.Select(r => $"user:{r}").ToList();
|
||||||
|
|
||||||
|
foreach (var recipientId in recipients)
|
||||||
|
{
|
||||||
|
await Clients.Groups(recipientsStrings)
|
||||||
|
.SendAsync(eventName, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to load friend list for notifications. userId: {userId}", userId);
|
_logger.LogError(ex,
|
||||||
|
"Failed to notify profile update for user {UserId}",
|
||||||
|
UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
groups = await _groupsRepository.GetByUserIdAsync(userId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to load group list for notifications. userId: {userId}", userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var groupIds = groups.Select(g => g.Id).ToList();
|
|
||||||
|
|
||||||
// Current user
|
|
||||||
await Clients.Group($"user-{userId}")
|
|
||||||
.SendAsync(eventName, payload);
|
|
||||||
|
|
||||||
// Friends
|
|
||||||
foreach (var friend in friendIds)
|
|
||||||
{
|
|
||||||
await Clients.Group($"friends-{friend.Id}")
|
|
||||||
.SendAsync(eventName, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Groups
|
|
||||||
foreach (var groupId in groupIds)
|
|
||||||
{
|
|
||||||
await Clients.Group($"group-{groupId}")
|
|
||||||
.SendAsync(eventName, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Sent {EventName} for {UserId} to {Friends} friends and {Groups} groups",
|
|
||||||
eventName, userId, friendIds.Count(), groupIds.Count);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ builder.Services.AddCors(options =>
|
|||||||
policy.WithOrigins(
|
policy.WithOrigins(
|
||||||
"https://localhost:7155",
|
"https://localhost:7155",
|
||||||
"http://localhost:7155",
|
"http://localhost:7155",
|
||||||
|
"http://192.168.1.107:8080",
|
||||||
|
"http://0.0.0.0:8080",
|
||||||
"https://govor-team-govor-8ce1.twc1.net",
|
"https://govor-team-govor-8ce1.twc1.net",
|
||||||
"http://govor-team-govor-8ce1.twc1.net"
|
"http://govor-team-govor-8ce1.twc1.net"
|
||||||
)
|
)
|
||||||
@@ -113,6 +115,7 @@ if (!app.Environment.IsDevelopment())
|
|||||||
{
|
{
|
||||||
//app.MapOpenApi();
|
//app.MapOpenApi();
|
||||||
builder.WebHost.UseUrls("http://0.0.0.0:8080");
|
builder.WebHost.UseUrls("http://0.0.0.0:8080");
|
||||||
|
builder.WebHost.UseUrls("http://192.168.1.107:8080");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
@@ -132,6 +135,7 @@ app.MapControllers();
|
|||||||
app.MapHub<ChatsHub>("/hubs/chats");
|
app.MapHub<ChatsHub>("/hubs/chats");
|
||||||
app.MapHub<FriendsHub>("/hubs/friends");
|
app.MapHub<FriendsHub>("/hubs/friends");
|
||||||
app.MapHub<ProfileHub>("/hubs/profiles");
|
app.MapHub<ProfileHub>("/hubs/profiles");
|
||||||
|
app.MapHub<PresenceHub>("/hubs/presence");
|
||||||
|
|
||||||
app.MapSwagger()
|
app.MapSwagger()
|
||||||
.RequireAuthorization();
|
.RequireAuthorization();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://0.0.0.0:8080;",
|
"applicationUrl": "http://0.0.0.0:8080;http://localhost:7155;http://192.168.1.107:8080;72.56.93.242:8080;",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "https://0.0.0.0:8080;",
|
"applicationUrl": "https://0.0.0.0:8080; https://localhost:7155",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"GovorDbContext": "Host=localhost;Port=5432;Database=GovorDb;Username=postgres;Password=stalcker;"
|
"GovorDbContext": "Host=72.56.93.203;Port=5432;Database=default_db;Username=gen_user;Password=9_-L%OLk+v(rQj;"
|
||||||
},
|
},
|
||||||
"UseMySql": false,
|
"UseMySql": false,
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
@@ -18,6 +18,6 @@
|
|||||||
"RefreshTokenLifetimeDays": 30
|
"RefreshTokenLifetimeDays": 30
|
||||||
},
|
},
|
||||||
"EncryptionOption": {
|
"EncryptionOption": {
|
||||||
"Secret": "4r8B2j9kP5mX7nQwE2zY3A=="
|
"Secret": "8B2j9kkw9xP5m7nQwE2zY3A-=Q8zP7C4+TqLZpg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using Govor.Application.Services;
|
||||||
|
|
||||||
|
namespace Govor.Application.Tests.Services;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class SynchingServiceTests
|
||||||
|
{
|
||||||
|
private readonly SynchingService _sut; // System Under Test
|
||||||
|
|
||||||
|
public SynchingServiceTests()
|
||||||
|
{
|
||||||
|
_sut = new SynchingService();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NormalizeNewlines_ShouldReturnEmptyString_WhenInputIsEmpty()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = _sut.NormalizeNewlines(string.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NormalizeNewlines_ShouldReturnNull_WhenInputIsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string input = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.NormalizeNewlines(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NormalizeNewlines_ShouldNotChangeUnixStyleNewlines_WhenInputIsLF()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string input = "Line 1\nLine 2\nLine 3";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.NormalizeNewlines(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NormalizeNewlines_ShouldConvertWindowsStyleNewlines_WhenInputIsCRLF()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string input = "Line 1\r\nLine 2\r\nLine 3";
|
||||||
|
const string expected = "Line 1\nLine 2\nLine 3";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.NormalizeNewlines(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NormalizeNewlines_ShouldConvertMacStyleNewlines_WhenInputIsCR()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string input = "Line 1\rLine 2\rLine 3";
|
||||||
|
const string expected = "Line 1\nLine 2\nLine 3";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.NormalizeNewlines(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NormalizeNewlines_ShouldHandleMixedNewlines_WhenInputIsMixed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string input = "Line 1\r\nLine 2\rLine 3\nLine 4";
|
||||||
|
const string expected = "Line 1\nLine 2\nLine 3\nLine 4";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.NormalizeNewlines(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NormalizeNewlines_ShouldHandleTextWithoutNewlines()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string input = "This is a single line of text.";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.NormalizeNewlines(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(input));
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-5
@@ -9,11 +9,11 @@ using Moq;
|
|||||||
namespace Govor.Application.Tests.Services;
|
namespace Govor.Application.Tests.Services;
|
||||||
|
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class UserGroupsServiceTests
|
public class UserGroupsGetterServiceTests
|
||||||
{
|
{
|
||||||
private Fixture _fixture;
|
private Fixture _fixture;
|
||||||
private Mock<IGroupsRepository> _repositoryMock;
|
private Mock<IGroupsRepository> _repositoryMock;
|
||||||
private IUserGroupsService _service;
|
private IUserGroupsGetterService _getterService;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
@@ -29,7 +29,7 @@ public class UserGroupsServiceTests
|
|||||||
|
|
||||||
_repositoryMock = new Mock<IGroupsRepository>();
|
_repositoryMock = new Mock<IGroupsRepository>();
|
||||||
|
|
||||||
_service = new UserGroupsService(_repositoryMock.Object);
|
_getterService = new UserGroupsGetterService(_repositoryMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -43,7 +43,7 @@ public class UserGroupsServiceTests
|
|||||||
.ReturnsAsync([chats.First()]);
|
.ReturnsAsync([chats.First()]);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _service.GetUserGroupsAsync(userId);
|
var result = await _getterService.GetUserGroupsAsync(userId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Is.Not.Null);
|
Assert.That(result, Is.Not.Null);
|
||||||
@@ -61,7 +61,7 @@ public class UserGroupsServiceTests
|
|||||||
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
|
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _service.GetUserGroupsAsync(userId);
|
var result = await _getterService.GetUserGroupsAsync(userId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result, Is.Not.Null);
|
Assert.That(result, Is.Not.Null);
|
||||||
@@ -20,6 +20,7 @@ public class UserSessionRefresherTests
|
|||||||
private Mock<IJwtService> _jwtServiceMock;
|
private Mock<IJwtService> _jwtServiceMock;
|
||||||
private Mock<ILogger<UserSessionRefresher>> _loggerMock;
|
private Mock<ILogger<UserSessionRefresher>> _loggerMock;
|
||||||
private Mock<IOptions<JwtRefreshOption>> _optionsMock;
|
private Mock<IOptions<JwtRefreshOption>> _optionsMock;
|
||||||
|
private Mock<IJwtTokenHasher> _jwtTokenHasherMock;
|
||||||
private JwtRefreshOption _options;
|
private JwtRefreshOption _options;
|
||||||
private UserSessionRefresher _refresher;
|
private UserSessionRefresher _refresher;
|
||||||
private const string OldRefreshToken = "old-refresh-token";
|
private const string OldRefreshToken = "old-refresh-token";
|
||||||
@@ -36,6 +37,7 @@ public class UserSessionRefresherTests
|
|||||||
_jwtServiceMock = new Mock<IJwtService>();
|
_jwtServiceMock = new Mock<IJwtService>();
|
||||||
_loggerMock = new Mock<ILogger<UserSessionRefresher>>();
|
_loggerMock = new Mock<ILogger<UserSessionRefresher>>();
|
||||||
_optionsMock = new Mock<IOptions<JwtRefreshOption>>();
|
_optionsMock = new Mock<IOptions<JwtRefreshOption>>();
|
||||||
|
_jwtTokenHasherMock = new Mock<IJwtTokenHasher>();
|
||||||
|
|
||||||
_options = new JwtRefreshOption { RefreshTokenLifetimeDays = 30 };
|
_options = new JwtRefreshOption { RefreshTokenLifetimeDays = 30 };
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ public class UserSessionRefresherTests
|
|||||||
_loggerMock.Object,
|
_loggerMock.Object,
|
||||||
_usersRepoMock.Object,
|
_usersRepoMock.Object,
|
||||||
_optionsMock.Object,
|
_optionsMock.Object,
|
||||||
|
_jwtTokenHasherMock.Object,
|
||||||
_jwtServiceMock.Object);
|
_jwtServiceMock.Object);
|
||||||
|
|
||||||
_user = new User
|
_user = new User
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ namespace Govor.Application.Interfaces.Friends;
|
|||||||
public interface IFriendshipService
|
public interface IFriendshipService
|
||||||
{
|
{
|
||||||
Task<List<User>> GetFriendsAsync(Guid userId);
|
Task<List<User>> GetFriendsAsync(Guid userId);
|
||||||
|
Task<List<User>> GetPotentialFriendsAsync(Guid userId);
|
||||||
Task<List<User>> SearchUsersAsync(string query, Guid currentId);
|
Task<List<User>> SearchUsersAsync(string query, Guid currentId);
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,6 @@ namespace Govor.Application.Interfaces;
|
|||||||
|
|
||||||
public interface IMessagesLoader
|
public interface IMessagesLoader
|
||||||
{
|
{
|
||||||
Task<List<Message>> LoadMessagesInUserChat(Guid userId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2);
|
Task<List<Message>> LoadMessagesInUserChat(Guid privateChatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2);
|
||||||
Task<List<Message>> LoadMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2);
|
Task<List<Message>> LoadMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Govor.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface ISynchingService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Brings all line breaks (CRLF, CR) to a single LF(\n) standard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The original line containing the line break.</param>
|
||||||
|
/// <returns>A string normalized using only \n.</returns>
|
||||||
|
string NormalizeNewlines(string input);
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@ using Govor.Core.Models;
|
|||||||
|
|
||||||
namespace Govor.Application.Interfaces;
|
namespace Govor.Application.Interfaces;
|
||||||
|
|
||||||
public interface IUserGroupsService
|
public interface IUserGroupsGetterService
|
||||||
{
|
{
|
||||||
Task<List<ChatGroup>> GetUserGroupsAsync(Guid userId);
|
Task<List<ChatGroup>> GetUserGroupsAsync(Guid userId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Govor.Core.Models;
|
||||||
|
|
||||||
|
namespace Govor.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserPrivateChatsGetterService
|
||||||
|
{
|
||||||
|
Task<List<PrivateChat>> GetUserChatsAsync(Guid userId);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ public interface IMediaService
|
|||||||
public Task DeleteMediaAsync(Guid fileId);
|
public Task DeleteMediaAsync(Guid fileId);
|
||||||
public Task<Media> GetMediaByUrlAsync(string url);
|
public Task<Media> GetMediaByUrlAsync(string url);
|
||||||
public Task<Media> GetMediaByIdAsync(Guid mediaId);
|
public Task<Media> GetMediaByIdAsync(Guid mediaId);
|
||||||
|
public Task<bool> HasMediaAsync(Guid mediaId);
|
||||||
|
public Task<bool> HasMediaByUrlAsync(string url);
|
||||||
Task AttachToMessageAsync(Guid mediaId, Guid messageId);
|
Task AttachToMessageAsync(Guid mediaId, Guid messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ namespace Govor.Application.Interfaces.UserOnlineStatus;
|
|||||||
|
|
||||||
public interface IOnlineUserStore
|
public interface IOnlineUserStore
|
||||||
{
|
{
|
||||||
void SetOnlineUser(Guid userId);
|
bool AddConnection(Guid userId, string connectionId);
|
||||||
void SetOfflineUser(Guid userId);
|
bool RemoveConnection(Guid userId, string connectionId);
|
||||||
bool IsOnline(Guid userId);
|
bool IsOnline(Guid userId);
|
||||||
|
IEnumerable<string> GetConnections(Guid userId);
|
||||||
IReadOnlyCollection<Guid> GetAllOnlineUsers();
|
IReadOnlyCollection<Guid> GetAllOnlineUsers();
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,32 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using Govor.Application.Interfaces.Authentication;
|
using Govor.Application.Interfaces.Authentication;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Govor.Application.Services.Authentication;
|
namespace Govor.Application.Services.Authentication;
|
||||||
|
|
||||||
public class JwtTokenHasher : IJwtTokenHasher
|
public class JwtTokenHasher : IJwtTokenHasher
|
||||||
{
|
{
|
||||||
|
private readonly string _pepper;
|
||||||
|
|
||||||
|
public JwtTokenHasher(IConfiguration config)
|
||||||
|
{
|
||||||
|
_pepper = config["EncryptionOption:Secret"] ?? "D1fault%Lxng%Randxm^Secret^Key(123!";
|
||||||
|
}
|
||||||
|
|
||||||
public string HashToken(string token)
|
public string HashToken(string token)
|
||||||
{
|
{
|
||||||
return BCrypt.Net.BCrypt.HashPassword(token, workFactor: 13);
|
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_pepper));
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(token);
|
||||||
|
var hash = hmac.ComputeHash(bytes);
|
||||||
|
return Convert.ToBase64String(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool VerifyToken(string token, string storedHash)
|
public bool VerifyToken(string token, string storedHash)
|
||||||
{
|
{
|
||||||
return BCrypt.Net.BCrypt.Verify(token, storedHash);
|
var currentHashBytes = Encoding.UTF8.GetBytes(HashToken(token));
|
||||||
|
var storedHashBytes = Encoding.UTF8.GetBytes(storedHash);
|
||||||
|
|
||||||
|
return CryptographicOperations.FixedTimeEquals(currentHashBytes, storedHashBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,18 +20,33 @@ public class FriendRequestCommandService : IFriendRequestCommandService
|
|||||||
if (fromUserId == toUserId)
|
if (fromUserId == toUserId)
|
||||||
throw new InvalidOperationException("Cannot send a request to self user");
|
throw new InvalidOperationException("Cannot send a request to self user");
|
||||||
|
|
||||||
if (_friendshipsRepository.Exist(fromUserId, toUserId))
|
var friendship = await _friendshipsRepository.GetFriendshipAsync(fromUserId, toUserId);
|
||||||
throw new RequestAlreadySentException(fromUserId, toUserId);
|
|
||||||
|
|
||||||
var friendship = new Friendship
|
if (friendship is null)
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
friendship = new Friendship
|
||||||
RequesterId = fromUserId,
|
{
|
||||||
AddresseeId = toUserId,
|
Id = Guid.NewGuid(),
|
||||||
Status = FriendshipStatus.Pending
|
RequesterId = fromUserId,
|
||||||
};
|
AddresseeId = toUserId,
|
||||||
|
Status = FriendshipStatus.Pending
|
||||||
|
};
|
||||||
|
await _friendshipsRepository.AddAsync(friendship);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (friendship.Status == FriendshipStatus.Pending || friendship.Status == FriendshipStatus.Accepted
|
||||||
|
|| friendship.Status == FriendshipStatus.Blocked)
|
||||||
|
{
|
||||||
|
throw new RequestAlreadySentException(fromUserId, toUserId);
|
||||||
|
}
|
||||||
|
|
||||||
await _friendshipsRepository.AddAsync(friendship);
|
friendship.RequesterId = fromUserId;
|
||||||
|
friendship.AddresseeId = toUserId;
|
||||||
|
friendship.Status = FriendshipStatus.Pending;
|
||||||
|
|
||||||
|
await _friendshipsRepository.UpdateAsync(friendship);
|
||||||
|
}
|
||||||
|
|
||||||
return friendship;
|
return friendship;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class FriendshipService : IFriendshipService
|
|||||||
_friendshipsRepository = friendshipsRepository;
|
_friendshipsRepository = friendshipsRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<User>> SearchUsersAsync(string query, Guid currentId)
|
public async Task<List<User>> SearchUsersAsync(string query, Guid currentId)
|
||||||
{
|
{
|
||||||
List<User> all = new List<User>();
|
List<User> all = new List<User>();
|
||||||
@@ -43,6 +44,23 @@ public class FriendshipService : IFriendshipService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<User>> GetPotentialFriendsAsync(Guid userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var friendships = await _friendshipsRepository.FindByUserIdAsync(userId);
|
||||||
|
|
||||||
|
return friendships
|
||||||
|
.Where(f => f.Status == FriendshipStatus.Pending)
|
||||||
|
.Select(f => f.RequesterId == userId ? f.Addressee : f.Requester)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
catch (NotFoundByKeyException<Guid> ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Nothing was found for the specified id.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<User>> GetFriendsAsync(Guid userId)
|
public async Task<List<User>> GetFriendsAsync(Guid userId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -100,6 +100,19 @@ public class MediaService : IMediaService
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasMediaAsync(Guid mediaId)
|
||||||
|
{
|
||||||
|
return await _dbContext.MediaFiles.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == mediaId) is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasMediaByUrlAsync(string url)
|
||||||
|
{
|
||||||
|
return await _dbContext.MediaFiles.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Url == url) is not null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AttachToMessageAsync(Guid mediaId, Guid messageId)
|
public async Task AttachToMessageAsync(Guid mediaId, Guid messageId)
|
||||||
{
|
{
|
||||||
var mediaFile = await _dbContext.MediaFiles
|
var mediaFile = await _dbContext.MediaFiles
|
||||||
|
|||||||
@@ -51,12 +51,22 @@ public class MessageCommandService : IMessageCommandService
|
|||||||
{
|
{
|
||||||
if (!await _usersRepository.ExistsByIdAsync(sendParams.RecipientId))
|
if (!await _usersRepository.ExistsByIdAsync(sendParams.RecipientId))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Attempt to send message to non-existent user {RecipientId}", sendParams.RecipientId);
|
if (!_privateChats.Exist(sendParams.RecipientId))
|
||||||
return new SendMessageResult(false, new KeyNotFoundException($"Recipient user {sendParams.RecipientId} not found."), default);
|
{
|
||||||
|
_logger.LogWarning("Attempt to send message to non-existent user {RecipientId}", sendParams.RecipientId);
|
||||||
|
return new SendMessageResult(false, new KeyNotFoundException($"Recipient user {sendParams.RecipientId} not found."), default);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
recipientId = sendParams.RecipientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Verify friendship for private messages
|
||||||
|
await _verifyFriendship.VerifyAsync(sendParams.FromUserId, sendParams.RecipientId);
|
||||||
|
recipientId = await GetPrivateChatsIdAsync(sendParams.FromUserId, sendParams.RecipientId);
|
||||||
}
|
}
|
||||||
// Verify friendship for private messages
|
|
||||||
await _verifyFriendship.VerifyAsync(sendParams.FromUserId, sendParams.RecipientId);
|
|
||||||
recipientId = await GetPrivateChatsIdAsync(sendParams.FromUserId, sendParams.RecipientId);
|
|
||||||
}
|
}
|
||||||
else if (sendParams.RecipientType == RecipientType.Group)
|
else if (sendParams.RecipientType == RecipientType.Group)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,26 +25,24 @@ public class MessagesLoader : IMessagesLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Message>> LoadMessagesInUserChat(
|
public async Task<List<Message>> LoadMessagesInUserChat(
|
||||||
Guid userId,
|
Guid privateChatId,
|
||||||
Guid currentUser,
|
Guid currentUser,
|
||||||
Guid? startMessageId,
|
Guid? startMessageId,
|
||||||
int before = 20,
|
int before = 20,
|
||||||
int after = 2)
|
int after = 2)
|
||||||
{
|
{
|
||||||
if (userId == Guid.Empty)
|
if (privateChatId == Guid.Empty)
|
||||||
throw new ArgumentException("User id cannot be empty");
|
throw new ArgumentException("PrivateChatId id cannot be empty");
|
||||||
|
|
||||||
if (!_privateChatsRepository.Exist(userId, currentUser))
|
if (!_privateChatsRepository.Exist(privateChatId))
|
||||||
throw new InvalidOperationException("Private chat not found");
|
throw new InvalidOperationException("Private chat not found");
|
||||||
|
|
||||||
var chat = await _privateChatsRepository.GetByMembersAsync(userId, currentUser);
|
|
||||||
|
|
||||||
var query = _dbContext.Messages
|
var query = _dbContext.Messages
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(m => m.MediaAttachments)
|
.Include(m => m.MediaAttachments)
|
||||||
.ThenInclude(m => m.MediaFile)
|
.ThenInclude(m => m.MediaFile)
|
||||||
.Where(m => m.RecipientType == RecipientType.User &&
|
.Where(m => m.RecipientType == RecipientType.User &&
|
||||||
m.RecipientId == chat.Id);
|
m.RecipientId == privateChatId);
|
||||||
|
|
||||||
if (startMessageId is null)
|
if (startMessageId is null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Govor.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace Govor.Application.Services;
|
||||||
|
|
||||||
|
public class SynchingService : ISynchingService
|
||||||
|
{
|
||||||
|
public string NormalizeNewlines(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalized = input.Replace("\r\n", "\n");
|
||||||
|
|
||||||
|
normalized = normalized.Replace('\r', '\n');
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -5,11 +5,11 @@ using Govor.Data.Repositories.Exceptions;
|
|||||||
|
|
||||||
namespace Govor.Application.Services;
|
namespace Govor.Application.Services;
|
||||||
|
|
||||||
public class UserGroupsService : IUserGroupsService
|
public class UserGroupsGetterService : IUserGroupsGetterService
|
||||||
{
|
{
|
||||||
private readonly IGroupsRepository _groupRep;
|
private readonly IGroupsRepository _groupRep;
|
||||||
|
|
||||||
public UserGroupsService(IGroupsRepository groupsRepository)
|
public UserGroupsGetterService(IGroupsRepository groupsRepository)
|
||||||
{
|
{
|
||||||
_groupRep = groupsRepository;
|
_groupRep = groupsRepository;
|
||||||
}
|
}
|
||||||
@@ -5,25 +5,68 @@ namespace Govor.Application.Services.UserOnlineStatus;
|
|||||||
|
|
||||||
public class OnlineUserStore : IOnlineUserStore
|
public class OnlineUserStore : IOnlineUserStore
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<Guid, DateTime> _onlineUsers = new();
|
private readonly ConcurrentDictionary<Guid, HashSet<string>> _userConnections = new();
|
||||||
|
|
||||||
public void SetOnlineUser(Guid userId)
|
public bool AddConnection(Guid userId, string connectionId)
|
||||||
{
|
{
|
||||||
_onlineUsers[userId] = DateTime.UtcNow;
|
bool isFirstConnection = false;
|
||||||
|
|
||||||
|
_userConnections.AddOrUpdate(userId,
|
||||||
|
_ => {
|
||||||
|
isFirstConnection = true;
|
||||||
|
return new HashSet<string> { connectionId };
|
||||||
|
},
|
||||||
|
(_, connections) => {
|
||||||
|
lock (connections)
|
||||||
|
{
|
||||||
|
if (connections.Count == 0) isFirstConnection = true;
|
||||||
|
connections.Add(connectionId);
|
||||||
|
}
|
||||||
|
return connections;
|
||||||
|
});
|
||||||
|
|
||||||
|
return isFirstConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetOfflineUser(Guid userId)
|
public bool RemoveConnection(Guid userId, string connectionId)
|
||||||
{
|
{
|
||||||
_onlineUsers.TryRemove(userId, out _);
|
bool isLastConnection = false;
|
||||||
|
|
||||||
|
if (_userConnections.TryGetValue(userId, out var connections))
|
||||||
|
{
|
||||||
|
lock (connections)
|
||||||
|
{
|
||||||
|
connections.Remove(connectionId);
|
||||||
|
if (connections.Count == 0)
|
||||||
|
{
|
||||||
|
isLastConnection = true;
|
||||||
|
_userConnections.TryRemove(userId, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLastConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetConnections(Guid userId)
|
||||||
|
{
|
||||||
|
if (_userConnections.TryGetValue(userId, out var connections))
|
||||||
|
{
|
||||||
|
lock (connections)
|
||||||
|
{
|
||||||
|
return connections.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Enumerable.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsOnline(Guid userId)
|
public bool IsOnline(Guid userId)
|
||||||
{
|
{
|
||||||
return _onlineUsers.ContainsKey(userId);
|
return _userConnections.TryGetValue(userId, out var connections) && connections.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyCollection<Guid> GetAllOnlineUsers()
|
public IReadOnlyCollection<Guid> GetAllOnlineUsers()
|
||||||
{
|
{
|
||||||
return _onlineUsers.Keys.ToList();
|
return _userConnections.Keys.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ public class UserNotificationScopeService : IUserNotificationScopeService
|
|||||||
}
|
}
|
||||||
catch (NotFoundByKeyException<Guid> ex)
|
catch (NotFoundByKeyException<Guid> ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, ex.Message);
|
_logger.LogWarning(ex, ex.Message);
|
||||||
throw new InvalidOperationException("User not found");
|
throw new InvalidOperationException("User not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Govor.Application.Interfaces;
|
||||||
|
using Govor.Core.Models;
|
||||||
|
using Govor.Core.Repositories.PrivateChats;
|
||||||
|
using Govor.Data.Repositories.Exceptions;
|
||||||
|
|
||||||
|
namespace Govor.Application.Services;
|
||||||
|
|
||||||
|
public class UserPrivateChatsGetter : IUserPrivateChatsGetterService
|
||||||
|
{
|
||||||
|
private readonly IPrivateChatsRepository _groupRep;
|
||||||
|
|
||||||
|
public UserPrivateChatsGetter(IPrivateChatsRepository groupsRepository)
|
||||||
|
{
|
||||||
|
_groupRep = groupsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<PrivateChat>> GetUserChatsAsync(Guid userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _groupRep.GetAllOfUser(userId);
|
||||||
|
}
|
||||||
|
catch (NotFoundByKeyException<Guid> ex)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ public class UserSessionRefresher : IUserSessionRefresher
|
|||||||
private readonly ILogger<UserSessionRefresher> _logger;
|
private readonly ILogger<UserSessionRefresher> _logger;
|
||||||
private readonly IUsersRepository _usersRepository;
|
private readonly IUsersRepository _usersRepository;
|
||||||
private readonly JwtRefreshOption _options;
|
private readonly JwtRefreshOption _options;
|
||||||
|
private readonly IJwtTokenHasher _jwtTokenHasher;
|
||||||
private readonly IJwtService _jwtService;
|
private readonly IJwtService _jwtService;
|
||||||
|
|
||||||
public UserSessionRefresher(
|
public UserSessionRefresher(
|
||||||
@@ -23,12 +24,14 @@ public class UserSessionRefresher : IUserSessionRefresher
|
|||||||
ILogger<UserSessionRefresher> logger,
|
ILogger<UserSessionRefresher> logger,
|
||||||
IUsersRepository usersRepository,
|
IUsersRepository usersRepository,
|
||||||
IOptions<JwtRefreshOption> options,
|
IOptions<JwtRefreshOption> options,
|
||||||
|
IJwtTokenHasher jwtTokenHasher,
|
||||||
IJwtService jwtService)
|
IJwtService jwtService)
|
||||||
{
|
{
|
||||||
_sessionsRepository = sessionsRepository;
|
_sessionsRepository = sessionsRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_usersRepository = usersRepository;
|
_usersRepository = usersRepository;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
|
_jwtTokenHasher = jwtTokenHasher;
|
||||||
_jwtService = jwtService;
|
_jwtService = jwtService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +39,8 @@ public class UserSessionRefresher : IUserSessionRefresher
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var session = await _sessionsRepository.GetByHashedRefreshTokenAsync(refreshToken);
|
|
||||||
|
var session = await _sessionsRepository.GetByHashedRefreshTokenAsync(_jwtTokenHasher.HashToken(refreshToken));
|
||||||
|
|
||||||
if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow)
|
if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow)
|
||||||
throw new UnauthorizedAccessException("Refresh token is invalid or expired");
|
throw new UnauthorizedAccessException("Refresh token is invalid or expired");
|
||||||
@@ -51,11 +55,13 @@ public class UserSessionRefresher : IUserSessionRefresher
|
|||||||
var newAccessToken = await _jwtService.GenerateAccessTokenAsync(user, session.Id);
|
var newAccessToken = await _jwtService.GenerateAccessTokenAsync(user, session.Id);
|
||||||
var newRefreshToken = await _jwtService.GenerateRefreshTokenAsync(user);
|
var newRefreshToken = await _jwtService.GenerateRefreshTokenAsync(user);
|
||||||
|
|
||||||
|
var newRefreshTokenHash = _jwtTokenHasher.HashToken(newRefreshToken);
|
||||||
|
|
||||||
// Opening new session
|
// Opening new session
|
||||||
var newSession = new UserSession
|
var newSession = new UserSession
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
RefreshTokenHash = newRefreshToken,
|
RefreshTokenHash = newRefreshTokenHash,
|
||||||
DeviceInfo = session.DeviceInfo,
|
DeviceInfo = session.DeviceInfo,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays)
|
ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays)
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ public class SendMessageCommand : IInteractiveCommand
|
|||||||
|
|
||||||
public string LongHelp()
|
public string LongHelp()
|
||||||
{
|
{
|
||||||
return "Отпарвка тестовых сообщений не существующему юзеру 2";
|
return "Отпарвка тестовых сообщений существующему юзеру 2";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ShortHelp()
|
public string ShortHelp()
|
||||||
{
|
{
|
||||||
return "Отпарвка тестовых сообщений не существующему юзеру";
|
return "Отпарвка тестовых сообщений существующему юзеру";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Govor.Contracts.DTOs;
|
||||||
|
|
||||||
|
public class AvatarUpdatePayload
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public Guid IconId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Govor.Contracts.DTOs;
|
||||||
|
|
||||||
|
public class DescriptionUpdatePayload
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ public class UserProfileDto
|
|||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public Guid? IconId { get; set; }
|
public Guid? IconId { get; set; }
|
||||||
|
public bool IsOnline { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ using Govor.Core.Models.Messages;
|
|||||||
|
|
||||||
namespace Govor.Contracts.Responses.SignalR;
|
namespace Govor.Contracts.Responses.SignalR;
|
||||||
|
|
||||||
public record UserMessageResponse
|
public class UserMessageResponse
|
||||||
{
|
{
|
||||||
public Guid MessageId { get; init; }
|
public Guid MessageId { get; set; }
|
||||||
public Guid SenderId { get; init; }
|
public Guid SenderId { get; set; }
|
||||||
public Guid RecipientId { get; init; }
|
public Guid RecipientId { get; set; }
|
||||||
public RecipientType RecipientType{get; init; }
|
public RecipientType RecipientType{get; set; }
|
||||||
public string EncryptedContent { get; init; } = string.Empty;
|
public string EncryptedContent { get; set; } = string.Empty;
|
||||||
public Guid? ReplyToMessageId { get; init; }
|
public Guid? ReplyToMessageId { get; set; }
|
||||||
public DateTime SentAt { get; init; }
|
public DateTime SentAt { get; set; }
|
||||||
public bool IsEdited { get; init; } = false;
|
public bool IsEdited { get; set; } = false;
|
||||||
public List<MediaFile> MediaAttachments { get; init; } = new List<MediaFile>();
|
public List<MediaFile> MediaAttachments { get; set; } = new List<MediaFile>();
|
||||||
}
|
}
|
||||||
@@ -20,16 +20,6 @@ public class User
|
|||||||
{
|
{
|
||||||
var user = obj as User;
|
var user = obj as User;
|
||||||
|
|
||||||
return Id == user.Id &&
|
return Id == user.Id;
|
||||||
Username == user.Username &&
|
|
||||||
Description == user.Description &&
|
|
||||||
PasswordHash == user.PasswordHash &&
|
|
||||||
IconId == user.IconId &&
|
|
||||||
CreatedOn == user.CreatedOn &&
|
|
||||||
WasOnline == user.WasOnline &&
|
|
||||||
InviteId == user.InviteId &&
|
|
||||||
Invite == user.Invite &&
|
|
||||||
SentFriendRequests == user.SentFriendRequests &&
|
|
||||||
ReceivedFriendRequests == user.ReceivedFriendRequests;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,5 @@ namespace Govor.Core.Repositories.Friendships;
|
|||||||
public interface IFriendshipsExist
|
public interface IFriendshipsExist
|
||||||
{
|
{
|
||||||
bool Exist(Guid requesterId, Guid addresseeId);
|
bool Exist(Guid requesterId, Guid addresseeId);
|
||||||
|
bool IsPossibilityOfCreatingFriendship(Guid requesterId, Guid addresseeId);
|
||||||
}
|
}
|
||||||
@@ -6,5 +6,6 @@ public interface IFriendshipsReader
|
|||||||
{
|
{
|
||||||
Task<List<Friendship>> GetAllAsync();
|
Task<List<Friendship>> GetAllAsync();
|
||||||
Task<Friendship> GetByIdAsync(Guid id);
|
Task<Friendship> GetByIdAsync(Guid id);
|
||||||
|
Task<Friendship> GetFriendshipAsync(Guid fromUserId, Guid toUserId);
|
||||||
Task<List<Friendship>> FindByUserIdAsync(Guid userId);
|
Task<List<Friendship>> FindByUserIdAsync(Guid userId);
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,5 @@ public interface IPrivateChatsReader
|
|||||||
Task<List<PrivateChat>> GetAllAsync();
|
Task<List<PrivateChat>> GetAllAsync();
|
||||||
Task<PrivateChat> GetByIdAsync(Guid id);
|
Task<PrivateChat> GetByIdAsync(Guid id);
|
||||||
Task<PrivateChat> GetByMembersAsync(Guid memberAId, Guid memberBId);
|
Task<PrivateChat> GetByMembersAsync(Guid memberAId, Guid memberBId);
|
||||||
|
Task<List<PrivateChat>> GetAllOfUser(Guid userId);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,10 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
{
|
{
|
||||||
builder.HasKey(x => x.Id);
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.HasIndex(u => u.Username).IsUnique();
|
||||||
|
builder.HasIndex(u => u.CreatedOn);
|
||||||
|
builder.HasIndex(u => u.WasOnline);
|
||||||
|
|
||||||
builder.HasOne(u => u.Invite)
|
builder.HasOne(u => u.Invite)
|
||||||
.WithMany(i => i.Users)
|
.WithMany(i => i.Users)
|
||||||
.HasForeignKey(u => u.InviteId);
|
.HasForeignKey(u => u.InviteId);
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ public class FriendshipsRepository : IFriendshipsRepository
|
|||||||
?? throw new NotFoundByKeyException<Guid>(id, "Friendship with given id was not found");
|
?? throw new NotFoundByKeyException<Guid>(id, "Friendship with given id was not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Friendship> GetFriendshipAsync(Guid fromUserId, Guid toUserId)
|
||||||
|
{
|
||||||
|
return await _context.Friendships
|
||||||
|
.FirstOrDefaultAsync(x =>
|
||||||
|
(x.RequesterId == fromUserId && x.AddresseeId == toUserId) ||
|
||||||
|
(x.RequesterId == toUserId && x.AddresseeId == fromUserId));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<Friendship>> FindByUserIdAsync(Guid userId)
|
public async Task<List<Friendship>> FindByUserIdAsync(Guid userId)
|
||||||
{
|
{
|
||||||
return await _context.Friendships
|
return await _context.Friendships
|
||||||
@@ -120,4 +128,23 @@ public class FriendshipsRepository : IFriendshipsRepository
|
|||||||
return _context.Friendships.AsNoTracking().Any(x => (x.RequesterId == requesterId && x.AddresseeId == addresseeId) ||
|
return _context.Friendships.AsNoTracking().Any(x => (x.RequesterId == requesterId && x.AddresseeId == addresseeId) ||
|
||||||
x.RequesterId == addresseeId && x.AddresseeId == requesterId);
|
x.RequesterId == addresseeId && x.AddresseeId == requesterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsPossibilityOfCreatingFriendship(Guid requesterId, Guid addresseeId)
|
||||||
|
{
|
||||||
|
// Ищем любую существующую связь между пользователями
|
||||||
|
var existingFriendship = _context.Friendships
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefault(x =>
|
||||||
|
(x.RequesterId == requesterId && x.AddresseeId == addresseeId) ||
|
||||||
|
(x.RequesterId == addresseeId && x.AddresseeId == requesterId));
|
||||||
|
|
||||||
|
// Если записи нет — создавать можно
|
||||||
|
if (existingFriendship == null) return true;
|
||||||
|
|
||||||
|
// Если запись есть, создавать новую НЕЛЬЗЯ (нужно обновлять старую),
|
||||||
|
// за исключением случаев, когда статус "Rejected" или "Blocked" (смотря какая логика)
|
||||||
|
// Но обычно метод "IsPossibility" подразумевает именно "можно ли инициировать процесс"
|
||||||
|
|
||||||
|
return existingFriendship.Status == FriendshipStatus.Rejected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,14 @@ public class PrivateChatsRepository : IPrivateChatsRepository
|
|||||||
?? throw new NotFoundByKeyException<(Guid, Guid)>((memberAId, memberBId), "Private Chat with given members Id's does not exist");
|
?? throw new NotFoundByKeyException<(Guid, Guid)>((memberAId, memberBId), "Private Chat with given members Id's does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<PrivateChat>> GetAllOfUser(Guid userId)
|
||||||
|
{
|
||||||
|
return await _context.PrivateChats
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(f => f.UserAId == userId || f.UserBId == userId)
|
||||||
|
.ToListOrThrowIfEmpty(new NotFoundByKeyException<Guid>(userId, "Private Chat with given member Id's does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AddAsync(PrivateChat chat)
|
public async Task AddAsync(PrivateChat chat)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -102,8 +102,6 @@ public class UserSessionsRepository : IUserSessionsRepository
|
|||||||
|
|
||||||
if (rowsAffected == 0)
|
if (rowsAffected == 0)
|
||||||
throw new UpdateException($"Not found user session by given id {userSession.Id}");
|
throw new UpdateException($"Not found user session by given id {userSession.Id}");
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Govor.Core.Infrastructure.Extensions;
|
using Govor.Core.Infrastructure.Extensions;
|
||||||
using Govor.Core.Infrastructure.Validators;
|
using Govor.Core.Infrastructure.Validators;
|
||||||
|
using Govor.Core.Models;
|
||||||
using Govor.Core.Models.Users;
|
using Govor.Core.Models.Users;
|
||||||
using Govor.Core.Repositories.Users;
|
using Govor.Core.Repositories.Users;
|
||||||
using Govor.Data.Repositories.Exceptions;
|
using Govor.Data.Repositories.Exceptions;
|
||||||
@@ -77,16 +78,13 @@ public class UsersRepository : IUsersRepository
|
|||||||
{
|
{
|
||||||
return await _context.Users
|
return await _context.Users
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(u => u.Invite)
|
|
||||||
.Include(u => u.ReceivedFriendRequests)
|
|
||||||
.Include(u => u.SentFriendRequests)
|
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Where(u => u.Id != currentUserId &&
|
.Where(u => u.Id != currentUserId &&
|
||||||
u.Username.ToLower().Contains(query.ToLower()) &&
|
u.Username.ToLower().Contains(query.ToLower()) &&
|
||||||
!_context.Friendships.Any(f =>
|
!_context.Friendships.Any(f =>
|
||||||
(f.RequesterId == currentUserId && f.AddresseeId == u.Id) ||
|
((f.RequesterId == currentUserId && f.AddresseeId == u.Id) ||
|
||||||
(f.RequesterId == u.Id && f.AddresseeId == currentUserId)))
|
(f.RequesterId == u.Id && f.AddresseeId == currentUserId)) && f.Status != FriendshipStatus.Rejected))
|
||||||
.Take(20)
|
.Take(10)
|
||||||
.OrderBy(u => u.Username)
|
.OrderBy(u => u.Username)
|
||||||
.ToListOrThrowIfEmpty(new NotFoundByKeyException<(string, Guid)>((query, currentUserId), $"Users with given query for user {currentUserId} not found"));
|
.ToListOrThrowIfEmpty(new NotFoundByKeyException<(string, Guid)>((query, currentUserId), $"Users with given query for user {currentUserId} not found"));
|
||||||
}
|
}
|
||||||
@@ -162,8 +160,6 @@ public class UsersRepository : IUsersRepository
|
|||||||
|
|
||||||
if (rowsAffected == 0)
|
if (rowsAffected == 0)
|
||||||
throw new UpdateException($"Not found user by given id {user.Id}");
|
throw new UpdateException($"Not found user by given id {user.Id}");
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user