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:
@@ -5,5 +5,6 @@ namespace Govor.Application.Interfaces.Friends;
|
||||
public interface IFriendshipService
|
||||
{
|
||||
Task<List<User>> GetFriendsAsync(Guid userId);
|
||||
Task<List<User>> GetPotentialFriendsAsync(Guid userId);
|
||||
Task<List<User>> SearchUsersAsync(string query, Guid currentId);
|
||||
}
|
||||
@@ -4,6 +4,6 @@ namespace Govor.Application.Interfaces;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
public interface IUserGroupsService
|
||||
public interface IUserGroupsGetterService
|
||||
{
|
||||
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<Media> GetMediaByUrlAsync(string url);
|
||||
public Task<Media> GetMediaByIdAsync(Guid mediaId);
|
||||
public Task<bool> HasMediaAsync(Guid mediaId);
|
||||
public Task<bool> HasMediaByUrlAsync(string url);
|
||||
Task AttachToMessageAsync(Guid mediaId, Guid messageId);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ namespace Govor.Application.Interfaces.UserOnlineStatus;
|
||||
|
||||
public interface IOnlineUserStore
|
||||
{
|
||||
void SetOnlineUser(Guid userId);
|
||||
void SetOfflineUser(Guid userId);
|
||||
bool AddConnection(Guid userId, string connectionId);
|
||||
bool RemoveConnection(Guid userId, string connectionId);
|
||||
bool IsOnline(Guid userId);
|
||||
IEnumerable<string> GetConnections(Guid userId);
|
||||
IReadOnlyCollection<Guid> GetAllOnlineUsers();
|
||||
}
|
||||
@@ -1,16 +1,32 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Govor.Application.Services.Authentication;
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -19,20 +19,35 @@ public class FriendRequestCommandService : IFriendRequestCommandService
|
||||
{
|
||||
if (fromUserId == toUserId)
|
||||
throw new InvalidOperationException("Cannot send a request to self user");
|
||||
|
||||
var friendship = await _friendshipsRepository.GetFriendshipAsync(fromUserId, toUserId);
|
||||
|
||||
if (_friendshipsRepository.Exist(fromUserId, toUserId))
|
||||
throw new RequestAlreadySentException(fromUserId, toUserId);
|
||||
|
||||
var friendship = new Friendship
|
||||
if (friendship is null)
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RequesterId = fromUserId,
|
||||
AddresseeId = toUserId,
|
||||
Status = FriendshipStatus.Pending
|
||||
};
|
||||
|
||||
await _friendshipsRepository.AddAsync(friendship);
|
||||
friendship = new Friendship
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
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);
|
||||
}
|
||||
|
||||
friendship.RequesterId = fromUserId;
|
||||
friendship.AddresseeId = toUserId;
|
||||
friendship.Status = FriendshipStatus.Pending;
|
||||
|
||||
await _friendshipsRepository.UpdateAsync(friendship);
|
||||
}
|
||||
|
||||
return friendship;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public class FriendshipService : IFriendshipService
|
||||
_friendshipsRepository = friendshipsRepository;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<User>> SearchUsersAsync(string query, Guid currentId)
|
||||
{
|
||||
List<User> all = new List<User>();
|
||||
@@ -42,6 +43,23 @@ public class FriendshipService : IFriendshipService
|
||||
throw new UnauthorizedAccessException($"When we try find friends by pattern {query} something wrong", ex);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -100,6 +100,19 @@ public class MediaService : IMediaService
|
||||
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)
|
||||
{
|
||||
var mediaFile = await _dbContext.MediaFiles
|
||||
|
||||
@@ -51,12 +51,22 @@ public class MessageCommandService : IMessageCommandService
|
||||
{
|
||||
if (!await _usersRepository.ExistsByIdAsync(sendParams.RecipientId))
|
||||
{
|
||||
_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);
|
||||
if (!_privateChats.Exist(sendParams.RecipientId))
|
||||
{
|
||||
_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)
|
||||
{
|
||||
|
||||
@@ -25,26 +25,24 @@ public class MessagesLoader : IMessagesLoader
|
||||
}
|
||||
|
||||
public async Task<List<Message>> LoadMessagesInUserChat(
|
||||
Guid userId,
|
||||
Guid privateChatId,
|
||||
Guid currentUser,
|
||||
Guid? startMessageId,
|
||||
int before = 20,
|
||||
int after = 2)
|
||||
{
|
||||
if (userId == Guid.Empty)
|
||||
throw new ArgumentException("User id cannot be empty");
|
||||
if (privateChatId == Guid.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");
|
||||
|
||||
var chat = await _privateChatsRepository.GetByMembersAsync(userId, currentUser);
|
||||
|
||||
|
||||
var query = _dbContext.Messages
|
||||
.AsNoTracking()
|
||||
.Include(m => m.MediaAttachments)
|
||||
.ThenInclude(m => m.MediaFile)
|
||||
.Where(m => m.RecipientType == RecipientType.User &&
|
||||
m.RecipientId == chat.Id);
|
||||
m.RecipientId == privateChatId);
|
||||
|
||||
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;
|
||||
|
||||
public class UserGroupsService : IUserGroupsService
|
||||
public class UserGroupsGetterService : IUserGroupsGetterService
|
||||
{
|
||||
private readonly IGroupsRepository _groupRep;
|
||||
|
||||
public UserGroupsService(IGroupsRepository groupsRepository)
|
||||
public UserGroupsGetterService(IGroupsRepository groupsRepository)
|
||||
{
|
||||
_groupRep = groupsRepository;
|
||||
}
|
||||
@@ -5,25 +5,68 @@ namespace Govor.Application.Services.UserOnlineStatus;
|
||||
|
||||
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)
|
||||
{
|
||||
return _onlineUsers.ContainsKey(userId);
|
||||
return _userConnections.TryGetValue(userId, out var connections) && connections.Count > 0;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<Guid> GetAllOnlineUsers()
|
||||
{
|
||||
return _onlineUsers.Keys.ToList();
|
||||
return _userConnections.Keys.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public class UserNotificationScopeService : IUserNotificationScopeService
|
||||
}
|
||||
catch (NotFoundByKeyException<Guid> ex)
|
||||
{
|
||||
_logger.LogError(ex, ex.Message);
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
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 IUsersRepository _usersRepository;
|
||||
private readonly JwtRefreshOption _options;
|
||||
private readonly IJwtTokenHasher _jwtTokenHasher;
|
||||
private readonly IJwtService _jwtService;
|
||||
|
||||
public UserSessionRefresher(
|
||||
@@ -23,12 +24,14 @@ public class UserSessionRefresher : IUserSessionRefresher
|
||||
ILogger<UserSessionRefresher> logger,
|
||||
IUsersRepository usersRepository,
|
||||
IOptions<JwtRefreshOption> options,
|
||||
IJwtTokenHasher jwtTokenHasher,
|
||||
IJwtService jwtService)
|
||||
{
|
||||
_sessionsRepository = sessionsRepository;
|
||||
_logger = logger;
|
||||
_usersRepository = usersRepository;
|
||||
_options = options.Value;
|
||||
_jwtTokenHasher = jwtTokenHasher;
|
||||
_jwtService = jwtService;
|
||||
}
|
||||
|
||||
@@ -36,7 +39,8 @@ public class UserSessionRefresher : IUserSessionRefresher
|
||||
{
|
||||
try
|
||||
{
|
||||
var session = await _sessionsRepository.GetByHashedRefreshTokenAsync(refreshToken);
|
||||
|
||||
var session = await _sessionsRepository.GetByHashedRefreshTokenAsync(_jwtTokenHasher.HashToken(refreshToken));
|
||||
|
||||
if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow)
|
||||
throw new UnauthorizedAccessException("Refresh token is invalid or expired");
|
||||
@@ -50,12 +54,14 @@ public class UserSessionRefresher : IUserSessionRefresher
|
||||
// New tokens
|
||||
var newAccessToken = await _jwtService.GenerateAccessTokenAsync(user, session.Id);
|
||||
var newRefreshToken = await _jwtService.GenerateRefreshTokenAsync(user);
|
||||
|
||||
|
||||
var newRefreshTokenHash = _jwtTokenHasher.HashToken(newRefreshToken);
|
||||
|
||||
// Opening new session
|
||||
var newSession = new UserSession
|
||||
{
|
||||
UserId = user.Id,
|
||||
RefreshTokenHash = newRefreshToken,
|
||||
RefreshTokenHash = newRefreshTokenHash,
|
||||
DeviceInfo = session.DeviceInfo,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays)
|
||||
|
||||
Reference in New Issue
Block a user