From 3ec61fa2c0558da04af001c22740a2243d4249a5 Mon Sep 17 00:00:00 2001 From: Artemy <109195690+stalcker2288969@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:28:42 +0700 Subject: [PATCH] Stable server was added removing messages many fixes bugs and moving to result pattern from throwing exceptions --- .../Controllers/AdminStuff/UsersController.cs | 11 +- Govor.API/Controllers/ChatLoadController.cs | 42 ++- .../Friends/FriendsRequestQueryController.cs | 1 - .../Friends/FriendshipController.cs | 18 +- Govor.API/Controllers/InviteController.cs | 4 +- Govor.API/Controllers/MediaController.cs | 47 ++-- Govor.API/Controllers/ProfileController.cs | 32 +-- Govor.API/Controllers/PushTokensController.cs | 5 +- Govor.API/Controllers/SessionController.cs | 38 +-- Govor.API/Govor.API.csproj | 4 + Govor.API/Hubs/ChatsHub.cs | 22 +- Govor.API/Hubs/FriendsHub.cs | 4 +- Govor.API/Hubs/ProfileHub.cs | 248 ++++++++++++------ Govor.API/Program.cs | 18 +- .../Authentication/AuthService.cs | 5 +- .../Authentication/InvitesService.cs | 5 +- .../Authentication/JWT/JwtService.cs | 3 +- Govor.Application/Medias/IMediaService.cs | 12 +- Govor.Application/Medias/MediaService.cs | 74 ++++-- .../Messages/IMessageRemovingService.cs | 5 +- Govor.Application/Messages/IMessagesLoader.cs | 6 +- .../Messages/MessageRemovingService.cs | 67 ++++- Govor.Application/Messages/MessagesLoader.cs | 14 +- .../Messages/Parameters/DeleteMessage.cs | 3 +- .../PushNotifications/PushTokenService.cs | 4 +- .../UserSessions/UserSessionRefresher.cs | 1 - .../Requests/SignalR/RemoveMessageRequest.cs | 1 + .../SignalR/RemoveMessageRequestType.cs | 7 + .../SignalR/MessageRemovedResponse.cs | 8 +- Govor.Domain/Common/Error.cs | 2 +- Govor.Domain/Common/ErrorType.cs | 3 +- ... 20260727130055_InitialCreate.Designer.cs} | 7 +- ...ate.cs => 20260727130055_InitialCreate.cs} | 3 +- .../Migrations/GovorDbContextModelSnapshot.cs | 5 +- Govor.Domain/Models/Invitation.cs | 1 + libs/SmartRes.dll | Bin 10752 -> 10240 bytes 36 files changed, 452 insertions(+), 278 deletions(-) create mode 100644 Govor.Contracts/Requests/SignalR/RemoveMessageRequestType.cs rename Govor.Domain/Migrations/{20260716110338_InitialCreate.Designer.cs => 20260727130055_InitialCreate.Designer.cs} (99%) rename Govor.Domain/Migrations/{20260716110338_InitialCreate.cs => 20260727130055_InitialCreate.cs} (99%) diff --git a/Govor.API/Controllers/AdminStuff/UsersController.cs b/Govor.API/Controllers/AdminStuff/UsersController.cs index 0a60b3d..4b94af6 100644 --- a/Govor.API/Controllers/AdminStuff/UsersController.cs +++ b/Govor.API/Controllers/AdminStuff/UsersController.cs @@ -9,22 +9,21 @@ namespace Govor.API.Controllers.AdminStuff; [ApiController] [Route("api/admin/[controller]")] -[Authorize(Roles = "Admin")] +[Authorize]//(Roles = "Admin") public class UsersController : Controller { private readonly ILogger _logger; private readonly IUsersAdministration _users; - - public UsersController(ILogger logger, - IUsersAdministration users, - IInvitationGenerator invitationGenerator) + public UsersController( + ILogger logger, + IUsersAdministration users) { _logger = logger; _users = users; } - [HttpGet] + [HttpGet("all")] public async Task AllUsers() { try diff --git a/Govor.API/Controllers/ChatLoadController.cs b/Govor.API/Controllers/ChatLoadController.cs index 550a8ce..cc3def1 100644 --- a/Govor.API/Controllers/ChatLoadController.cs +++ b/Govor.API/Controllers/ChatLoadController.cs @@ -1,10 +1,14 @@ using AutoMapper; +using Govor.API.Common.Extensions; using Govor.Application.Infrastructure.Extensions; using Govor.Application.Messages; using Govor.Contracts.Requests; using Govor.Contracts.Responses; +using Govor.Domain.Common; +using Govor.Domain.Models.Messages; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using SmartRes; namespace Govor.API.Controllers; @@ -40,21 +44,14 @@ public class ChatLoadController : Controller if (query.Before < 0 || query.After < 0 || query.After + query.Before > 100) return BadRequest("Values must be non-negative and total must not exceed 100."); - var result = await _messagesLoader.LoadMessagesInChatGroup( + var result = (await _messagesLoader.LoadMessagesInChatGroup( groupId, _currentUser.GetCurrentUserId(), query.StartMessageId, query.Before, - query.After); - - var response = _mapper.Map>(result); - - return Ok(response); - } - catch (ArgumentException ex) - { - _logger.LogWarning(ex, ex.Message); - return BadRequest(ex.Message); + query.After)).Map(messages => _mapper.Map>(messages)); + + return result.ToActionResult(); } catch (Exception ex) { @@ -73,21 +70,15 @@ public class ChatLoadController : Controller if (query.Before < 0 || query.After < 0 || query.After + query.Before > 100) return BadRequest("Values must be non-negative and total must not exceed 100."); - var result = await _messagesLoader.LoadMessagesInUserChat( - userId, - _currentUser.GetCurrentUserId(), - query.StartMessageId, - query.Before, - query.After); + var result = (await _messagesLoader.LoadMessagesInUserChat( + userId, + _currentUser.GetCurrentUserId(), + query.StartMessageId, + query.Before, + query.After) + ).Map(messages => _mapper.Map>(messages)); - var response = _mapper.Map>(result); - - return Ok(response); - } - catch (ArgumentException ex) - { - _logger.LogWarning(ex, ex.Message); - return BadRequest(ex.Message); + return result.ToActionResult(); } catch (Exception ex) { @@ -95,5 +86,4 @@ public class ChatLoadController : Controller return StatusCode(500, "Unexpected Error! Please try again later."); } } - } \ No newline at end of file diff --git a/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs b/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs index 2d1d367..6ad5263 100644 --- a/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs +++ b/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc; namespace Govor.API.Controllers.Friends; - [Authorize] [Route("api/friends")] [ApiController] diff --git a/Govor.API/Controllers/Friends/FriendshipController.cs b/Govor.API/Controllers/Friends/FriendshipController.cs index afbc129..31d3f62 100644 --- a/Govor.API/Controllers/Friends/FriendshipController.cs +++ b/Govor.API/Controllers/Friends/FriendshipController.cs @@ -2,11 +2,14 @@ using AutoMapper; using Govor.Application.Friends; using Govor.Application.Infrastructure.Extensions; using Govor.Contracts.DTOs; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Govor.API.Controllers.Friends; +[Authorize] [Route("api/friends")] +[ApiController] public class FriendshipController : Controller { private readonly ILogger _logger; @@ -40,11 +43,6 @@ public class FriendshipController : Controller return Ok(response); } - catch (UnauthorizedAccessException ex) - { - _logger.LogWarning(ex, ex.Message); - return Forbid(ex.Message); - } catch (Exception ex) { _logger.LogError(ex, ex.Message); @@ -63,16 +61,6 @@ public class FriendshipController : Controller return Ok(response); } - catch (InvalidOperationException ex) - { - _logger.LogError(ex, ex.Message); - return Ok(Array.Empty()); - } - catch (UnauthorizedAccessException ex) - { - _logger.LogWarning(ex, ex.Message); - return Forbid(ex.Message); - } catch (Exception ex) { _logger.LogError(ex, ex.Message); diff --git a/Govor.API/Controllers/InviteController.cs b/Govor.API/Controllers/InviteController.cs index e7de645..82fe52d 100644 --- a/Govor.API/Controllers/InviteController.cs +++ b/Govor.API/Controllers/InviteController.cs @@ -21,11 +21,11 @@ public class InviteController : ControllerBase [Authorize] [HttpGet("{code}")] - public IActionResult JoinGroup(string code) + public async Task JoinGroup(string code) { try { - _groupService.AddUserToGroupByInvitationAsync(_currentUser.GetCurrentUserId(), code); + var groupRes = await _groupService.AddUserToGroupByInvitationAsync(_currentUser.GetCurrentUserId(), code); var group = _groupService.GetGroupByInviteCode(code); diff --git a/Govor.API/Controllers/MediaController.cs b/Govor.API/Controllers/MediaController.cs index d6ab259..8607bee 100644 --- a/Govor.API/Controllers/MediaController.cs +++ b/Govor.API/Controllers/MediaController.cs @@ -1,3 +1,4 @@ +using Govor.API.Common.Extensions; using Govor.Application.Infrastructure.Extensions; using Govor.Application.Medias; using Govor.Contracts.Requests; @@ -70,19 +71,22 @@ public class MediaController : Controller _logger.LogInformation("Uploaded file {FileName} from user {UserId}", media.FileName, media.UploaderId); - - return Ok(result); + + if (result.IsFailure) + { + _logger.LogWarning("Uploaded file {FileName} from user {UserId} finished with error: {Error}", + media.FileName, + media.UploaderId, + result.Error); + } + + return result.ToActionResult(); } catch (UnauthorizedAccessException ex) { _logger.LogWarning(ex, ex.Message); return Forbid(ex.Message); } - catch (InvalidOperationException ex) - { - _logger.LogWarning(ex, ex.Message); - return BadRequest(ex.Message); - } catch (Exception ex) { _logger.LogError(ex, "Error uploading media"); @@ -101,26 +105,17 @@ public class MediaController : Controller [HttpGet("download/{id}")] public async Task Download(Guid id) { - try - { - var userId = _currentUserService.GetCurrentUserId(); + var userId = _currentUserService.GetCurrentUserId(); - if (!await _accesser.HasAccessAsync(id, userId)) - return Forbid(); + if (!await _accesser.HasAccessAsync(id, userId)) + return Forbid(); - var media = await _mediaService.GetMediaByIdAsync(id); - - return File(media.Data, media.MimeType, Path.GetFileName(media.FileName)); - } - catch (KeyNotFoundException ex) - { - _logger.LogWarning(ex, ex.Message); - return NotFound("Media not found"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error downloading media"); - return StatusCode(500, "Internal server error"); - } + var mediaResult = await _mediaService.GetMediaByIdAsync(id); + + if (mediaResult.IsFailure) + return mediaResult.ToActionResult(); + + var media = mediaResult.Value; + return File(media.Data, media.MimeType, Path.GetFileName(media.FileName)); } } diff --git a/Govor.API/Controllers/ProfileController.cs b/Govor.API/Controllers/ProfileController.cs index 0f45836..636805f 100644 --- a/Govor.API/Controllers/ProfileController.cs +++ b/Govor.API/Controllers/ProfileController.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Govor.API.Common.Extensions; using Govor.API.Hubs; using Govor.Application.Infrastructure.Extensions; using Govor.Application.Medias; @@ -9,6 +10,7 @@ using Govor.Domain.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using SmartRes; namespace Govor.API.Controllers; @@ -43,10 +45,9 @@ public class ProfileController : ControllerBase [HttpPost("avatar")] // api/profile/avatar public async Task UploadAvatar([FromForm] AvatarUploadRequest request) { - var userId = _currentUserService.GetCurrentUserId(); - if (request.FromFile == null || request.FromFile.Length == 0) + if (request?.FromFile == null || request.FromFile.Length == 0) { return BadRequest("File is empty."); } @@ -65,11 +66,13 @@ public class ProfileController : ControllerBase String.Empty, MediaOwnerType.Avatar, userId); - - var mediaInfo = await _mediaService.UploadMediaAsync(media); - await _profileService.SetNewIcon(userId, mediaInfo.MediaId); - - return Ok(mediaInfo); + + var mediaInfo = await _mediaService.UploadMediaAsync(media) + .TapAsync(m => _logger.LogInformation("Uploaded avatar file {filename} by user {id}.", + request.FromFile.FileName, userId)) + .TapAsync(mediaInfo => _profileService.SetNewIcon(userId, mediaInfo.MediaId)); + + return mediaInfo.ToActionResult(); } catch (System.Exception ex) { @@ -92,12 +95,9 @@ public class ProfileController : ControllerBase var userId = _currentUserService.GetCurrentUserId(); var result = await _profileService.GetUserProfileAsync(userId); - if(result.IsFailure) - return NotFound(result.Error); - - var user = result.Value; - var dto = _mapper.Map(user); - return Ok(dto); + return result + .Map(user => _mapper.Map(user)) + .ToActionResult(); } catch (UnauthorizedAccessException ex) { @@ -116,10 +116,10 @@ public class ProfileController : ControllerBase { try { - var user = await _profileService.GetUserProfileAsync(id); + var user = (await _profileService.GetUserProfileAsync(id)) + .Map(user => _mapper.Map(user)); - var dto = _mapper.Map(user); - return Ok(dto); + return user.ToActionResult(); } catch (UnauthorizedAccessException ex) { diff --git a/Govor.API/Controllers/PushTokensController.cs b/Govor.API/Controllers/PushTokensController.cs index 2b53ae4..5bd9103 100644 --- a/Govor.API/Controllers/PushTokensController.cs +++ b/Govor.API/Controllers/PushTokensController.cs @@ -1,3 +1,4 @@ +using Govor.API.Common.Extensions; using Govor.Application.Infrastructure.Extensions; using Govor.Application.PushNotifications; using Govor.Contracts.Requests; @@ -39,13 +40,13 @@ public class PushTokensController : Controller var currentId = _currentUser.GetCurrentUserId(); var currentSessionId = _currentSession.GetUserSessionId(); - await _pushTokenService.AddOrUpdateTokenAsync( + var result = await _pushTokenService.AddOrUpdateTokenAsync( userId: currentId, sessionId: currentSessionId, token: req.Token, platform: req.Platform); - return Ok(); + return result.ToActionResult(); } catch (ArgumentException ex) { diff --git a/Govor.API/Controllers/SessionController.cs b/Govor.API/Controllers/SessionController.cs index 7aacee4..a1d2b48 100644 --- a/Govor.API/Controllers/SessionController.cs +++ b/Govor.API/Controllers/SessionController.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Govor.API.Common.Extensions; using Govor.Application.Infrastructure.Extensions; using Govor.Application.Users.UserSessions; using Govor.Contracts.DTOs; @@ -40,12 +41,10 @@ public class SessionController : Controller { try { - var sessions = await _userSessionReader.GetAllSessionsAsync(_currentUserService.GetCurrentUserId()); - - if(sessions.IsFailure) - return NotFound(sessions.Error); - - return Ok(_mapper.Map>(sessions.Value)); + var sessions = (await _userSessionReader.GetAllSessionsAsync(_currentUserService.GetCurrentUserId())) + .Map(sessions => _mapper.Map>(sessions)); + + return sessions.ToActionResult(); } catch (UnauthorizedAccessException ex) { @@ -70,15 +69,7 @@ public class SessionController : Controller var res = await _userSessionRevoker.CloseSessionByIdAsync(sessionId, _currentUserService.GetCurrentUserId()); - if (res.IsFailure) - return BadRequest(res.Error); - - return Ok(); - } - catch (InvalidOperationException ex) - { - _logger.LogError(ex, ex.Message); - return BadRequest(ex.Message); + return res.ToActionResult(); } catch (UnauthorizedAccessException ex) { @@ -97,19 +88,11 @@ public class SessionController : Controller { try { - var res = await _userSessionRevoker.CloseSessionByIdAsync( + var res = await _userSessionRevoker.CloseSessionByIdAsync( _currentUserSessionService.GetUserSessionId(), _currentUserService.GetCurrentUserId()); - if(res.IsFailure) - return NotFound(res.Error); - - return Ok(); - } - catch (InvalidOperationException ex) - { - _logger.LogError(ex, ex.Message); - return BadRequest(ex.Message); + return res.ToActionResult(); } catch (UnauthorizedAccessException ex) { @@ -130,10 +113,7 @@ public class SessionController : Controller { var res = await _userSessionRevoker.CloseAllSessionsAsync(_currentUserService.GetCurrentUserId()); - if(res.IsFailure) - return NotFound(res.Error); - - return Ok(); + return res.ToActionResult(); } catch (UnauthorizedAccessException ex) { diff --git a/Govor.API/Govor.API.csproj b/Govor.API/Govor.API.csproj index 1d39339..468fa36 100644 --- a/Govor.API/Govor.API.csproj +++ b/Govor.API/Govor.API.csproj @@ -33,5 +33,9 @@ + + + + diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index 02bb92c..7de557e 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -91,17 +91,27 @@ public class ChatsHub : Hub { return await SafeExecute(async (userId) => { - var result = await _messageRemovingService.DeleteMessageAsync(new DeleteMessage(userId, request.MessageId)); + var result = await _messageRemovingService.DeleteMessageAsync( + new DeleteMessage( + userId, + request.MessageId, + ForceRemove: request.RequestType switch + { + RemoveMessageRequestType.HideForMe => false, + RemoveMessageRequestType.ForceRemove => true, + _ => false + }) + ); - if (!result.IsSuccess || result.OriginalMessage == null) - throw new InvalidOperationException("Message deletion failed"); + if (!result.IsSuccess) + throw new InvalidOperationException(result.Error.ToString()); var notification = new MessageRemovedResponse { MessageId = request.MessageId, - SenderId = result.OriginalMessage.SenderId, - RecipientId = result.OriginalMessage.RecipientId, - RecipientType = result.OriginalMessage.RecipientType + SenderId = result.Value.SenderId, + RecipientId = result.Value.RecipientId, + RecipientType = result.Value.RecipientType }; await _notifier.NotifyMessageRemovedAsync(notification); diff --git a/Govor.API/Hubs/FriendsHub.cs b/Govor.API/Hubs/FriendsHub.cs index 9871926..cdd240d 100644 --- a/Govor.API/Hubs/FriendsHub.cs +++ b/Govor.API/Hubs/FriendsHub.cs @@ -82,7 +82,7 @@ public class FriendsHub : Hub return HubResult.Error(result.Error.ToString()); var friendship = result.Value; - var dto = _mapper.Map(friendship); + var dto = _mapper.Map(friendship); await Clients.Group(targetUserId.ToString()) .SendAsync("FriendRequestReceived", dto); @@ -127,7 +127,7 @@ public class FriendsHub : Hub var friendship = result.Value; - var dto = _mapper.Map(friendship); + var dto = _mapper.Map(friendship); await Clients.Group(userId.ToString()) .SendAsync("FriendRequestAccepted", dto); diff --git a/Govor.API/Hubs/ProfileHub.cs b/Govor.API/Hubs/ProfileHub.cs index 024f840..fdf531b 100644 --- a/Govor.API/Hubs/ProfileHub.cs +++ b/Govor.API/Hubs/ProfileHub.cs @@ -11,24 +11,29 @@ using Microsoft.AspNetCore.SignalR; namespace Govor.API.Hubs; [Authorize] -public class ProfileHub : Hub +public sealed class ProfileHub : Hub { - private readonly IFriendshipService _friendsService; + private const string UserGroupPrefix = "user:"; + + private const string DescriptionUpdatedEvent = "DescriptionUpdated"; + private const string AvatarUpdatedEvent = "AvatarUpdated"; + + private readonly IFriendshipService _friendshipService; private readonly IProfileService _profileService; private readonly IHubUserAccessor _userAccessor; private readonly ISynchingService _synchingService; + private readonly IMediaService _mediaService; private readonly ILogger _logger; - private readonly IMediaService _mediaService; - + public ProfileHub( - IFriendshipService friendsService, + IFriendshipService friendshipService, IProfileService profileService, IHubUserAccessor userAccessor, ISynchingService synchingService, IMediaService mediaService, ILogger logger) { - _friendsService = friendsService; + _friendshipService = friendshipService; _profileService = profileService; _userAccessor = userAccessor; _synchingService = synchingService; @@ -39,32 +44,66 @@ public class ProfileHub : Hub public override async Task OnConnectedAsync() { var userId = _userAccessor.GetUserId(Context); + var groupName = GetUserGroup(userId); - await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}"); + await Groups.AddToGroupAsync( + Context.ConnectionId, + groupName); + + _logger.LogDebug( + "User {UserId} connected to ProfileHub with connection {ConnectionId}", + userId, + Context.ConnectionId); await base.OnConnectedAsync(); } - public override async Task OnDisconnectedAsync(Exception exception) + public override async Task OnDisconnectedAsync(Exception? exception) { var userId = _userAccessor.GetUserId(Context); - - await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{userId}"); - + var groupName = GetUserGroup(userId); + + await Groups.RemoveFromGroupAsync( + Context.ConnectionId, + groupName); + + if (exception is not null) + { + _logger.LogWarning( + exception, + "User {UserId} disconnected from ProfileHub with connection {ConnectionId}", + userId, + Context.ConnectionId); + } + else + { + _logger.LogDebug( + "User {UserId} disconnected from ProfileHub with connection {ConnectionId}", + userId, + Context.ConnectionId); + } + await base.OnDisconnectedAsync(exception); } - - public async Task> SetDescription(string description) + + public async Task> SetDescription(string? description) { + if (description is null) + return HubResult.BadRequest("Description cannot be null."); + + description = _synchingService.NormalizeNewlines(description); + if (description.Length > 500) - return HubResult.Error("Description length exceeded."); + return HubResult.BadRequest( + "Description length exceeded."); var userId = _userAccessor.GetUserId(Context); try { - description = _synchingService.NormalizeNewlines(description); - await _profileService.SetDescription(description, userId); + await _profileService.SetDescription( + description, + userId); var payload = new DescriptionUpdatePayload { @@ -72,105 +111,158 @@ public class ProfileHub : Hub Description = description }; - await NotifyProfileUpdatedAsync(userId, "DescriptionUpdated", payload); + await NotifyProfileUpdatedAsync( + userId, + DescriptionUpdatedEvent, + payload); return HubResult.Ok(true); } catch (Exception ex) { - _logger.LogError(ex, "Failed to update description for user {UserId}", userId); - return HubResult.Error("Server error."); + _logger.LogError( + ex, + "Failed to update description for user {UserId}", + userId); + + return HubResult.Error( + "Server error."); } } public async Task> SetAvatar(Guid iconId) { + if (iconId == Guid.Empty) + return HubResult.BadRequest( + "Invalid icon id."); + var userId = _userAccessor.GetUserId(Context); try { - if (iconId == Guid.Empty || !await _mediaService.HasMediaAsync(iconId)) - return HubResult.BadRequest("Invalid icon id."); + var mediaExists = await _mediaService.HasMediaAsync(iconId); - await _profileService.SetNewIcon(userId, iconId); + if (!mediaExists) + return HubResult.BadRequest( + "Invalid icon id."); - var payload = new AvatarUpdatePayload(){UserId = userId, IconId = iconId}; + await _profileService.SetNewIcon( + userId, + iconId); - await NotifyProfileUpdatedAsync(userId, "AvatarUpdated", payload); + var payload = new AvatarUpdatePayload + { + UserId = userId, + IconId = iconId + }; + + await NotifyProfileUpdatedAsync( + userId, + AvatarUpdatedEvent, + payload); return HubResult.Ok(true); } catch (Exception ex) { - _logger.LogError(ex, "An error occurred while updating the user's {userId} avatar {iconId}", userId, iconId); - return HubResult.Error("An unaccounted error on the server!"); + _logger.LogError( + ex, + "Failed to update avatar for user {UserId}. IconId: {IconId}", + userId, + iconId); + + return HubResult.Error( + "Server error."); } } - + private async Task NotifyProfileUpdatedAsync( - Guid UserId, + Guid userId, string eventName, object payload) { try { - var recipients = new HashSet(); + var recipients = await GetProfileUpdateRecipientsAsync(userId); - // 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); - } + if (recipients.Count == 0) + return; + var groups = recipients + .Select(GetUserGroup) + .ToArray(); - // 3. groups - /*var groups = await _groupsRepository.GetByUserIdAsync(authorUserId); - foreach (var group in groups) + await Clients + .Groups(groups) + .SendAsync(eventName, payload); + + _logger.LogDebug( + "Profile update {EventName} for user {UserId} sent to {RecipientCount} users", + eventName, + userId, + recipients.Count); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Failed to notify profile update for user {UserId}", + userId); + } + } + + private async Task> GetProfileUpdateRecipientsAsync( + Guid userId) + { + var recipients = new HashSet(); + + // User himself. + recipients.Add(userId); + + // Friends. + try + { + var friends = await _friendshipService.GetFriendsAsync(userId); + + foreach (var friend in friends) { - 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); + if (friend.Id != Guid.Empty) + recipients.Add(friend.Id); } } catch (Exception ex) { - _logger.LogError(ex, - "Failed to notify profile update for user {UserId}", - UserId); + _logger.LogWarning( + ex, + "Failed to get friends for user {UserId}", + userId); } + + // Potential friends. + try + { + var potentialFriends = + await _friendshipService.GetPotentialFriendsAsync(userId); + + foreach (var potentialFriend in potentialFriends) + { + if (potentialFriend.Id != Guid.Empty) + recipients.Add(potentialFriend.Id); + } + } + catch (Exception ex) + { + _logger.LogWarning( + ex, + "Failed to get potential friends for user {UserId}", + userId); + } + + return recipients; } -} + + private static string GetUserGroup(Guid userId) + { + return $"{UserGroupPrefix}{userId}"; + } +} \ No newline at end of file diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index c84e91b..d28cc98 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -16,7 +16,6 @@ var services = builder.Services; builder.AddLogger();// Serilog - builder.Configuration.AddJsonFile("configs/ban_usernames.json", optional: false, reloadOnChange: true); #if DEBUG @@ -65,7 +64,8 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; - if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/api/chats")) + + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) { context.Token = accessToken; } @@ -101,19 +101,9 @@ services.AddSwaggerGen(options => Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'" }); - options.AddSecurityRequirement(document => + options.AddSecurityRequirement(document => new OpenApiSecurityRequirement { - var requirement = new OpenApiSecurityRequirement - { - { - new OpenApiSecuritySchemeReference(schemeId) - { - Reference = new OpenApiReferenceWithDescription { Type = ReferenceType.SecurityScheme, Id = "Bearer" } - }, - [] - } - }; - return requirement; + [new OpenApiSecuritySchemeReference("Bearer", document)] = new List(0) }); }); diff --git a/Govor.Application/Authentication/AuthService.cs b/Govor.Application/Authentication/AuthService.cs index a5b6b88..316eca4 100644 --- a/Govor.Application/Authentication/AuthService.cs +++ b/Govor.Application/Authentication/AuthService.cs @@ -31,7 +31,6 @@ public class AuthService : IAccountService public async Task> RegistrationAsync(string name, string password, Invitation invitation) { - var validationResult = _usernameValidator.Validate(name); if (validationResult.IsFailure) { @@ -63,8 +62,8 @@ public class AuthService : IAccountService await _context.Users.AddAsync(user); await SetRoleAsync(user, invitation); - - // TODO: inv.participantCount -= 1; db.save(); + + invitation.Participants += 1; await _context.SaveChangesAsync(); diff --git a/Govor.Application/Authentication/InvitesService.cs b/Govor.Application/Authentication/InvitesService.cs index c4b4d60..5ea7942 100644 --- a/Govor.Application/Authentication/InvitesService.cs +++ b/Govor.Application/Authentication/InvitesService.cs @@ -1,4 +1,3 @@ -using Govor.Application.Exceptions.InvitesService; using Govor.Domain; using Govor.Domain.Common; using Govor.Domain.Models; @@ -24,7 +23,7 @@ public class InvitesService : IInvitesService public async Task GetRoleNameAsync(Guid sessionId) { - var invitation = await _context.Invitations.FirstOrDefaultAsync(s => s.Id == sessionId); + var invitation = await _context.Invitations.AsNoTracking().FirstOrDefaultAsync(s => s.Id == sessionId); if (invitation == null) return "User"; @@ -41,7 +40,7 @@ public class InvitesService : IInvitesService if (invite == null) return Result.Failure(Error.NotFound("Auth.LinkNotFount","Invitation not found.")); - if (invite.EndDate < DateTime.Now || invite.MaxParticipants <= invite.Users.Count) + if (invite.EndDate < DateTime.Now || invite.MaxParticipants <= invite.Users.Count || invite.MaxParticipants <= invite.Participants) { invite.IsActive = false; await _context.SaveChangesAsync(); diff --git a/Govor.Application/Authentication/JWT/JwtService.cs b/Govor.Application/Authentication/JWT/JwtService.cs index 609eddf..8938b32 100644 --- a/Govor.Application/Authentication/JWT/JwtService.cs +++ b/Govor.Application/Authentication/JWT/JwtService.cs @@ -26,7 +26,8 @@ public class JwtService : IJwtService { new Claim("userId", user.Id.ToString()), new Claim("sid", sessionId.ToString()), - new Claim(ClaimTypes.Role, await _invitesService.GetRoleNameAsync(user), ClaimValueTypes.String) + new Claim(ClaimTypes.Role, await _invitesService.GetRoleNameAsync(user)) + //new Claim(ClaimTypes.Role, await _invitesService.GetRoleNameAsync(user), ClaimValueTypes.String) }; var singing = new SigningCredentials( diff --git a/Govor.Application/Medias/IMediaService.cs b/Govor.Application/Medias/IMediaService.cs index 8e52df9..332ae7a 100644 --- a/Govor.Application/Medias/IMediaService.cs +++ b/Govor.Application/Medias/IMediaService.cs @@ -1,17 +1,19 @@ +using Govor.Domain.Common; using Govor.Domain.Models; using Govor.Domain.Models.Messages; +using SmartRes; namespace Govor.Application.Medias; public interface IMediaService { - public Task UploadMediaAsync(Media file); - public Task DeleteMediaAsync(Guid fileId); - public Task GetMediaByUrlAsync(string url); - public Task GetMediaByIdAsync(Guid mediaId); + public Task> UploadMediaAsync(Media file); + public Task> DeleteMediaAsync(Guid fileId); + public Task> GetMediaByUrlAsync(string url); + public Task> GetMediaByIdAsync(Guid mediaId); public Task HasMediaAsync(Guid mediaId); public Task HasMediaByUrlAsync(string url); - Task AttachToMessageAsync(Guid mediaId, Guid messageId); + public Task> AttachToMessageAsync(Guid mediaId, Guid messageId); } public record Media(Guid UploaderId, diff --git a/Govor.Application/Medias/MediaService.cs b/Govor.Application/Medias/MediaService.cs index b0adbd6..bc75350 100644 --- a/Govor.Application/Medias/MediaService.cs +++ b/Govor.Application/Medias/MediaService.cs @@ -1,8 +1,10 @@ using Govor.Application.Storage; using Govor.Domain.Models; using Govor.Domain; +using Govor.Domain.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.Medias; @@ -19,7 +21,7 @@ public class MediaService : IMediaService _logger = logger; } - public async Task UploadMediaAsync(Media file) + public async Task> UploadMediaAsync(Media file) { try { @@ -47,37 +49,56 @@ public class MediaService : IMediaService } catch (ArgumentException ex) { - throw new InvalidOperationException($"An error occured while uploading the media file: {ex.Message}"); + return Result.Failure(Error.Failure( + nameof(InvalidOperationException), + $"An error occured while uploading the media file: {ex.Message}") + ); } } - public async Task DeleteMediaAsync(Guid mediaId) + public async Task> DeleteMediaAsync(Guid mediaId) { var mediaFile = await _dbContext.MediaFiles - .FirstOrDefaultAsync(x => x.Id == mediaId) - ?? throw new KeyNotFoundException($"No media found by given id {mediaId}"); - + .FirstOrDefaultAsync(x => x.Id == mediaId); + + if (mediaFile is null) + { + return Result.Failure(Error.NotFound( + "File.DeleteMedia", + $"File with given id ({mediaId}) doesn't exist!") + ); + } + await _storageService.RemoveAsync(mediaFile.Url); _dbContext.MediaFiles.Remove(mediaFile); await _dbContext.SaveChangesAsync(); + + return new Unit(); } - public Task GetMediaByUrlAsync(string url) + public Task> GetMediaByUrlAsync(string url) { throw new NotImplementedException(); } - public async Task GetMediaByIdAsync(Guid mediaId) + public async Task> GetMediaByIdAsync(Guid mediaId) { try { var mediaFile = await _dbContext.MediaFiles - .AsNoTracking() - .FirstOrDefaultAsync(x => x.Id == mediaId) - ?? throw new KeyNotFoundException($"No media found by given id {mediaId}"); + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == mediaId); + if (mediaFile is null) + { + return Result.Failure(Error.NotFound( + "File.GetMediaById", + $"File with given id ({mediaId}) doesn't exist!") + ); + } + // Загрузить бинарные данные из хранилища Stream dataStream = await _storageService.LoadAsync(mediaFile.Url); @@ -86,7 +107,8 @@ public class MediaService : IMediaService await dataStream.CopyToAsync(memoryStream); var contentBytes = memoryStream.ToArray(); - _logger.LogInformation($"Media found: {mediaFile.MediaType} with id: {mediaFile.Id} and url: {mediaFile.Url}"); + _logger.LogInformation("Media found: {mediaFile.MediaType} with id: {mediaFile.Id} and url: {mediaFile.Url}", + mediaFile.MediaType, mediaFile.Id, mediaFile.Url); // Вернуть объект Media return new Media( @@ -103,33 +125,43 @@ public class MediaService : IMediaService } catch (FileNotFoundException ex) { - _logger.LogWarning(ex, "Media file not found on storage."); - throw; + _logger.LogWarning(ex, "Media file ({0}) not found on storage.", mediaId); + return Result.Failure(Error.ServerError("File.GetMediaById", $"Media file not found on storage!")); } } public async Task HasMediaAsync(Guid mediaId) { return await _dbContext.MediaFiles.AsNoTracking() - .FirstOrDefaultAsync(x => x.Id == mediaId) is not null; + .AnyAsync(x => x.Id == mediaId); } public async Task HasMediaByUrlAsync(string url) { return await _dbContext.MediaFiles.AsNoTracking() - .FirstOrDefaultAsync(x => x.Url == url) is not null; + .AnyAsync(x => x.Url == url); } - public async Task AttachToMessageAsync(Guid mediaId, Guid messageId) + public async Task> AttachToMessageAsync(Guid mediaId, Guid messageId) { var mediaFile = await _dbContext.MediaFiles - .FirstOrDefaultAsync(x => x.Id == mediaId) - ?? throw new KeyNotFoundException($"No media found by given id {mediaId}"); + .FirstOrDefaultAsync(x => x.Id == mediaId); + if (mediaFile is null) + { + return Result.Failure(Error.NotFound( + "File.AttachToMessage", + $"File with given id ({mediaId}) doesn't exist!") + ); + } + if (mediaFile.OwnerType != MediaOwnerType.Message) { _logger.LogWarning("Attempt to attach already owned media {MediaId}", mediaId); - throw new InvalidOperationException($"Media {mediaId} is already attached to {mediaFile.OwnerType}"); + return Result.Failure(Error.Failure( + "File.AttachToMessage", + $"Media {mediaId} is already attached to {mediaFile.OwnerType}") + ); } mediaFile.OwnerType = MediaOwnerType.Message; @@ -139,5 +171,7 @@ public class MediaService : IMediaService await _dbContext.SaveChangesAsync(); _logger.LogInformation("Media {MediaId} successfully attached to message {MessageId}", mediaId, messageId); + + return new Unit(); } } \ No newline at end of file diff --git a/Govor.Application/Messages/IMessageRemovingService.cs b/Govor.Application/Messages/IMessageRemovingService.cs index 041e71f..ea154c6 100644 --- a/Govor.Application/Messages/IMessageRemovingService.cs +++ b/Govor.Application/Messages/IMessageRemovingService.cs @@ -1,8 +1,11 @@ using Govor.Application.Messages.Parameters; +using Govor.Domain.Common; +using Govor.Domain.Models.Messages; +using SmartRes; namespace Govor.Application.Messages; public interface IMessageRemovingService { - Task DeleteMessageAsync(DeleteMessage deleteParams); + Task> DeleteMessageAsync(DeleteMessage deleteParams); } \ No newline at end of file diff --git a/Govor.Application/Messages/IMessagesLoader.cs b/Govor.Application/Messages/IMessagesLoader.cs index e6591a5..4648180 100644 --- a/Govor.Application/Messages/IMessagesLoader.cs +++ b/Govor.Application/Messages/IMessagesLoader.cs @@ -1,9 +1,11 @@ +using Govor.Domain.Common; using Govor.Domain.Models.Messages; +using SmartRes; namespace Govor.Application.Messages; public interface IMessagesLoader { - Task> LoadMessagesInUserChat(Guid privateChatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2); - Task> LoadMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2); + Task,Error>> LoadMessagesInUserChat(Guid privateChatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2); + Task,Error>> LoadMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2); } \ No newline at end of file diff --git a/Govor.Application/Messages/MessageRemovingService.cs b/Govor.Application/Messages/MessageRemovingService.cs index 51f6f59..32314a9 100644 --- a/Govor.Application/Messages/MessageRemovingService.cs +++ b/Govor.Application/Messages/MessageRemovingService.cs @@ -1,6 +1,10 @@ using Govor.Application.Messages.Parameters; using Govor.Domain; +using Govor.Domain.Common; +using Govor.Domain.Models.Messages; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.Messages; @@ -17,8 +21,67 @@ public class MessageRemovingService : IMessageRemovingService _logger = logger; } - public Task DeleteMessageAsync(DeleteMessage deleteParams) + public async Task> DeleteMessageAsync(DeleteMessage deleteParams) { - throw new NotImplementedException(); + var message = await _govorDbContext.Messages.FirstOrDefaultAsync(m => m.Id == deleteParams.MessageId); + + if (message == null) + return Result.Failure( + Error.NotFound( + "MessageRemoving", + "Message with given id doesn't exist") + ); + + var result = message.RecipientType switch + { + RecipientType.Group => await ValidateGroupRecipientAsync(message, deleteParams), + RecipientType.User => await ValidateUserRecipientAsync(message, deleteParams), + + _ => Result.Failure(Error.Failure( + "MessageRemoving.ArgumentOutOfRangeException", + $"Argument out of range: {nameof(message.RecipientType)}" + ) + ) + }; + + return result; + } + + private async Task> ValidateGroupRecipientAsync(Message message, DeleteMessage deleteParams) + { + if (deleteParams.DeleterId == message.RecipientId) + { + return await ForceRemoveAsync(message); + } + else + { + // TODO made admin rules + return Result.Failure(Error.Failure("MessageRemoving.HaveNoPermission", + $"You do not have permission to delete message {message.Id}" + )); + } + } + + private async Task> ValidateUserRecipientAsync(Message message, DeleteMessage deleteParams) + { + if (deleteParams.DeleterId == message.RecipientId) + { + return await ForceRemoveAsync(message); + } + else + { + // TODO made hide rules + return Result.Failure(Error.Failure("MessageRemoving.HaveNoPermission", + $"You do not have permission to delete message {message.Id}" + )); + } + } + + private async Task> ForceRemoveAsync(Message message) + { + _govorDbContext.Messages.Remove(message); + await _govorDbContext.SaveChangesAsync(); + + return message; } } \ No newline at end of file diff --git a/Govor.Application/Messages/MessagesLoader.cs b/Govor.Application/Messages/MessagesLoader.cs index ba11f1e..bf1a37a 100644 --- a/Govor.Application/Messages/MessagesLoader.cs +++ b/Govor.Application/Messages/MessagesLoader.cs @@ -1,7 +1,9 @@ using Govor.Application.Interfaces; using Govor.Domain.Models.Messages; using Govor.Domain; +using Govor.Domain.Common; using Microsoft.EntityFrameworkCore; +using SmartRes; namespace Govor.Application.Messages; @@ -14,7 +16,7 @@ public class MessagesLoader : IMessagesLoader _dbContext = dbContext; } - public async Task> LoadMessagesInUserChat( + public async Task,Error>> LoadMessagesInUserChat( Guid privateChatId, Guid currentUser, Guid? startMessageId, @@ -22,11 +24,11 @@ public class MessagesLoader : IMessagesLoader int after = 2) { if (privateChatId == Guid.Empty) - throw new ArgumentException("PrivateChatId id cannot be empty", nameof(privateChatId)); + return Result.Failure>(Error.Failure(nameof(ArgumentException),"PrivateChatId id cannot be empty.")); var chatExists = await _dbContext.PrivateChats.AnyAsync(c => c.Id == privateChatId); if (!chatExists) - return []; + return new List(0); var query = _dbContext.Messages .AsNoTracking() @@ -37,7 +39,7 @@ public class MessagesLoader : IMessagesLoader return await FetchPaginatedMessagesAsync(query, startMessageId, before, after); } - public async Task> LoadMessagesInChatGroup( + public async Task,Error>> LoadMessagesInChatGroup( Guid chatId, Guid currentUser, Guid? startMessageId, @@ -45,13 +47,13 @@ public class MessagesLoader : IMessagesLoader int after = 2) { if (chatId == Guid.Empty) - throw new ArgumentException("Chat id cannot be empty", nameof(chatId)); + return Result.Failure>(Error.Failure(nameof(ArgumentException),"Chat id cannot be empty.")); var isMember = await _dbContext.GroupMemberships .AnyAsync(gm => gm.UserId == currentUser && gm.GroupId == chatId); if (!isMember) - return []; + return new List(0); var query = _dbContext.Messages .AsNoTracking() diff --git a/Govor.Application/Messages/Parameters/DeleteMessage.cs b/Govor.Application/Messages/Parameters/DeleteMessage.cs index 409c848..86abda2 100644 --- a/Govor.Application/Messages/Parameters/DeleteMessage.cs +++ b/Govor.Application/Messages/Parameters/DeleteMessage.cs @@ -2,4 +2,5 @@ namespace Govor.Application.Messages.Parameters; public record DeleteMessage( Guid DeleterId, - Guid MessageId); \ No newline at end of file + Guid MessageId, + bool ForceRemove = false); diff --git a/Govor.Application/PushNotifications/PushTokenService.cs b/Govor.Application/PushNotifications/PushTokenService.cs index 11b1dea..1f6f66f 100644 --- a/Govor.Application/PushNotifications/PushTokenService.cs +++ b/Govor.Application/PushNotifications/PushTokenService.cs @@ -119,7 +119,9 @@ public class PushTokenService : IPushTokenService await _context.UserPushTokens .Where(t => tokens.Contains(t.Token)) .ExecuteDeleteAsync(); - + + await _context.SaveChangesAsync(); + return Result.Success(); } catch (Exception ex) diff --git a/Govor.Application/Users/UserSessions/UserSessionRefresher.cs b/Govor.Application/Users/UserSessions/UserSessionRefresher.cs index 3499d49..441c281 100644 --- a/Govor.Application/Users/UserSessions/UserSessionRefresher.cs +++ b/Govor.Application/Users/UserSessions/UserSessionRefresher.cs @@ -42,7 +42,6 @@ public class UserSessionRefresher : IUserSessionRefresher try { var session = await _context.UserSessions - .AsNoTracking() .Include(userSession => userSession.User) .FirstOrDefaultAsync(s => s.RefreshTokenHash == hashedToken); diff --git a/Govor.Contracts/Requests/SignalR/RemoveMessageRequest.cs b/Govor.Contracts/Requests/SignalR/RemoveMessageRequest.cs index 4948b75..91da80f 100644 --- a/Govor.Contracts/Requests/SignalR/RemoveMessageRequest.cs +++ b/Govor.Contracts/Requests/SignalR/RemoveMessageRequest.cs @@ -3,4 +3,5 @@ namespace Govor.Contracts.Requests.SignalR; public class RemoveMessageRequest { public Guid MessageId { get; set; } + public RemoveMessageRequestType RequestType { get; set; } } \ No newline at end of file diff --git a/Govor.Contracts/Requests/SignalR/RemoveMessageRequestType.cs b/Govor.Contracts/Requests/SignalR/RemoveMessageRequestType.cs new file mode 100644 index 0000000..13179a1 --- /dev/null +++ b/Govor.Contracts/Requests/SignalR/RemoveMessageRequestType.cs @@ -0,0 +1,7 @@ +namespace Govor.Contracts.Requests.SignalR; + +public enum RemoveMessageRequestType : int +{ + HideForMe = 0, + ForceRemove = 1 +} \ No newline at end of file diff --git a/Govor.Contracts/Responses/SignalR/MessageRemovedResponse.cs b/Govor.Contracts/Responses/SignalR/MessageRemovedResponse.cs index 07a72d4..7594d56 100644 --- a/Govor.Contracts/Responses/SignalR/MessageRemovedResponse.cs +++ b/Govor.Contracts/Responses/SignalR/MessageRemovedResponse.cs @@ -1,11 +1,13 @@ +using Govor.Contracts.Requests.SignalR; using Govor.Domain.Models.Messages; namespace Govor.Contracts.Responses.SignalR; public class MessageRemovedResponse { - public Guid MessageId { get; set; } - public Guid SenderId { get; set; } - public Guid RecipientId { get; set; } + public required Guid MessageId { get; set; } + public required Guid SenderId { get; set; } + public required Guid RecipientId { get; set; } + public RemoveMessageRequestType RequestType { get; set; } public RecipientType RecipientType { get; set; } } \ No newline at end of file diff --git a/Govor.Domain/Common/Error.cs b/Govor.Domain/Common/Error.cs index 242be7a..73f0a31 100644 --- a/Govor.Domain/Common/Error.cs +++ b/Govor.Domain/Common/Error.cs @@ -11,6 +11,6 @@ public record Error(string Code, string Message, ErrorType Type, Dictionary new(code, message, ErrorType.Unauthorized); public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden); public static Error Failure(string code, string message) => new(code, message, ErrorType.Failure); - + public static Error ServerError(string code, string message) => new(code, message, ErrorType.ServerError); public override string ToString() => $"{Code}: {Message}"; } \ No newline at end of file diff --git a/Govor.Domain/Common/ErrorType.cs b/Govor.Domain/Common/ErrorType.cs index ac228b8..9b98ee9 100644 --- a/Govor.Domain/Common/ErrorType.cs +++ b/Govor.Domain/Common/ErrorType.cs @@ -7,5 +7,6 @@ public enum ErrorType NotFound = 2, // (404 Not Found) Conflict = 3, // (409 Conflict) Unauthorized = 4, // (401 Unauthorized) - Forbidden = 5 // (403 Forbidden) + Forbidden = 5, // (403 Forbidden) + ServerError = 6 // (500 Server Error) } \ No newline at end of file diff --git a/Govor.Domain/Migrations/20260716110338_InitialCreate.Designer.cs b/Govor.Domain/Migrations/20260727130055_InitialCreate.Designer.cs similarity index 99% rename from Govor.Domain/Migrations/20260716110338_InitialCreate.Designer.cs rename to Govor.Domain/Migrations/20260727130055_InitialCreate.Designer.cs index 6cfef7d..f81fad5 100644 --- a/Govor.Domain/Migrations/20260716110338_InitialCreate.Designer.cs +++ b/Govor.Domain/Migrations/20260727130055_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Govor.Domain.Migrations { [DbContext(typeof(GovorDbContext))] - [Migration("20260716110338_InitialCreate")] + [Migration("20260727130055_InitialCreate")] partial class InitialCreate { /// @@ -20,7 +20,7 @@ namespace Govor.Domain.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("ProductVersion", "10.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -202,6 +202,9 @@ namespace Govor.Domain.Migrations b.Property("MaxParticipants") .HasColumnType("integer"); + b.Property("Participants") + .HasColumnType("integer"); + b.HasKey("Id"); b.ToTable("Invitations"); diff --git a/Govor.Domain/Migrations/20260716110338_InitialCreate.cs b/Govor.Domain/Migrations/20260727130055_InitialCreate.cs similarity index 99% rename from Govor.Domain/Migrations/20260716110338_InitialCreate.cs rename to Govor.Domain/Migrations/20260727130055_InitialCreate.cs index ead7690..f4fdd48 100644 --- a/Govor.Domain/Migrations/20260716110338_InitialCreate.cs +++ b/Govor.Domain/Migrations/20260727130055_InitialCreate.cs @@ -38,7 +38,8 @@ namespace Govor.Domain.Migrations Description = table.Column(type: "text", nullable: false), DateCreated = table.Column(type: "timestamp with time zone", nullable: false), EndDate = table.Column(type: "timestamp with time zone", nullable: false), - MaxParticipants = table.Column(type: "integer", nullable: false) + MaxParticipants = table.Column(type: "integer", nullable: false), + Participants = table.Column(type: "integer", nullable: false) }, constraints: table => { diff --git a/Govor.Domain/Migrations/GovorDbContextModelSnapshot.cs b/Govor.Domain/Migrations/GovorDbContextModelSnapshot.cs index 713c4e5..5e8d8c5 100644 --- a/Govor.Domain/Migrations/GovorDbContextModelSnapshot.cs +++ b/Govor.Domain/Migrations/GovorDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Govor.Domain.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("ProductVersion", "10.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -199,6 +199,9 @@ namespace Govor.Domain.Migrations b.Property("MaxParticipants") .HasColumnType("integer"); + b.Property("Participants") + .HasColumnType("integer"); + b.HasKey("Id"); b.ToTable("Invitations"); diff --git a/Govor.Domain/Models/Invitation.cs b/Govor.Domain/Models/Invitation.cs index b2ebf48..8b5e015 100644 --- a/Govor.Domain/Models/Invitation.cs +++ b/Govor.Domain/Models/Invitation.cs @@ -12,6 +12,7 @@ public class Invitation public DateTime DateCreated { get; set; } public DateTime EndDate { get; set; } public int MaxParticipants { get; set; } + public int Participants { get; set; } = 0; public List Users { get; set; } = new List(); public override bool Equals(object? obj) diff --git a/libs/SmartRes.dll b/libs/SmartRes.dll index e2c071e04405543715ea0a2a1261ecd125d6aa6d..9787406c26df9cd0d925cb4421ab7c0f2a71e8cb 100644 GIT binary patch literal 10240 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 literal 10752 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