test to make server

This commit is contained in:
Artemy
2026-02-08 22:30:29 +07:00
parent cc2921d257
commit 0a43e35797
56 changed files with 949 additions and 442 deletions
@@ -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;
}
}
@@ -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)