Мелкий рефактор

This commit is contained in:
Халимов Рустам
2026-03-19 01:49:55 +03:00
parent feef15c796
commit 4410f2d98b
44 changed files with 129 additions and 64 deletions
-6
View File
@@ -1,6 +0,0 @@
root = true
[*.cs]
csharp_prefer_braces = true:warning
csharp_style_namespace_declarations = file_scoped:warning
dotnet_diagnostic.IDE0011.severity = warning
@@ -28,11 +28,11 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUse
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<SuccessResponse>(new Error("User.NotFound", "User not found")); return Result.Failure<SuccessResponse>(IdentityErrors.UserNotFound);
} }
if (string.IsNullOrWhiteSpace(request.NewPassword)) if (string.IsNullOrWhiteSpace(request.NewPassword))
return Result.Failure<SuccessResponse>(new Error("InvalidPassword", "Password cannot be empty")); return Result.Failure<SuccessResponse>(DomainErrors.InvalidPassword);
var hash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); var hash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
user.ChangePassword(hash); user.ChangePassword(hash);
@@ -29,7 +29,7 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (targetUser == null) if (targetUser == null)
{ {
return Result.Failure<AdminUserDetailsDto>(new Error("User.NotFound", "User not found")); return Result.Failure<AdminUserDetailsDto>(IdentityErrors.UserNotFound);
} }
var messagesCount = await _chatsDbContext.Messages.CountAsync(m => m.SenderId == request.UserId, cancellationToken); var messagesCount = await _chatsDbContext.Messages.CountAsync(m => m.SenderId == request.UserId, cancellationToken);
@@ -1,3 +1,4 @@
using Host.Application.Stories;
using Knot.Shared.Kernel; using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Configuration; using Knot.Shared.Kernel.Configuration;
using Knot.Shared.Kernel.Constants; using Knot.Shared.Kernel.Constants;
@@ -31,11 +32,11 @@ internal sealed class HandshakeFederationCommandHandler : ICommandHandler<Handsh
return Task.FromResult(Result.Failure<HandshakeResponse>(new Error(Errors.DisabledByAdmin, "Federation is disabled"))); return Task.FromResult(Result.Failure<HandshakeResponse>(new Error(Errors.DisabledByAdmin, "Federation is disabled")));
if (string.IsNullOrEmpty(request.Request.Domain) || string.IsNullOrEmpty(request.Request.PublicKey)) if (string.IsNullOrEmpty(request.Request.Domain) || string.IsNullOrEmpty(request.Request.PublicKey))
return Task.FromResult(Result.Failure<HandshakeResponse>(new Error("Request.Invalid", "Domain and PublicKey are required"))); return Task.FromResult(Result.Failure<HandshakeResponse>(DomainErrors.RequestInvalid));
var allowedList = conf.AllowedDomains?.Select(d => d.Trim().ToLower()).ToList() ?? new System.Collections.Generic.List<string>(); var allowedList = conf.AllowedDomains?.Select(d => d.Trim().ToLower()).ToList() ?? new System.Collections.Generic.List<string>();
if (!allowedList.Contains(request.Request.Domain.ToLowerInvariant())) if (!allowedList.Contains(request.Request.Domain.ToLowerInvariant()))
return Task.FromResult(Result.Failure<HandshakeResponse>(new Error("Unauthorized", "Domain is not in the whitelist"))); return Task.FromResult(Result.Failure<HandshakeResponse>(StoryErrors.Unauthorized));
using var rsa = RSA.Create(2048); using var rsa = RSA.Create(2048);
var selfPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); var selfPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
@@ -1,3 +1,4 @@
using Host.Application.Stories;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -49,7 +50,7 @@ internal sealed class AddStoryReactionCommandHandler : ICommandHandler<AddStoryR
var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken); var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken);
if (story == null) if (story == null)
{ {
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); return Result.Failure<MessageResponse>(StoryErrors.StoryNotFound);
} }
var existing = await _context.StoryReactions var existing = await _context.StoryReactions
@@ -1,3 +1,4 @@
using Host.Application.Stories;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -49,7 +50,7 @@ internal sealed class AddStoryReplyCommandHandler : ICommandHandler<AddStoryRepl
var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken); var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken);
if (story == null) if (story == null)
{ {
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); return Result.Failure<MessageResponse>(StoryErrors.StoryNotFound);
} }
var reply = new StoryReply(request.StoryId, request.UserId, request.Content); var reply = new StoryReply(request.StoryId, request.UserId, request.Content);
@@ -1,3 +1,4 @@
using Host.Application.Stories;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -24,11 +25,11 @@ internal sealed class DeleteStoryCommandHandler : ICommandHandler<DeleteStoryCom
var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken); var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken);
if (story == null) if (story == null)
{ {
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); return Result.Failure<MessageResponse>(StoryErrors.StoryNotFound);
} }
if (story.UserId != request.UserId) if (story.UserId != request.UserId)
{ {
return Result.Failure<MessageResponse>(new Error("Unauthorized", "Unauthorized")); return Result.Failure<MessageResponse>(StoryErrors.Unauthorized);
} }
_context.Stories.Remove(story); _context.Stories.Remove(story);
@@ -1,3 +1,4 @@
using Host.Application.Stories;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -38,7 +39,7 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler<ViewStoryCommand
if (story == null) if (story == null)
{ {
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); return Result.Failure<MessageResponse>(StoryErrors.StoryNotFound);
} }
if (story.UserId == request.UserId) if (story.UserId == request.UserId)
@@ -1,3 +1,4 @@
using Host.Application.Stories;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -33,11 +34,11 @@ internal sealed class GetStoryRepliesQueryHandler : IQueryHandler<GetStoryReplie
if (story == null) if (story == null)
{ {
return Result.Failure<List<StoryReplyDto>>(new Error("Story.NotFound", "Story not found")); return Result.Failure<List<StoryReplyDto>>(StoryErrors.StoryNotFound);
} }
if (story.UserId != request.UserId) if (story.UserId != request.UserId)
{ {
return Result.Failure<List<StoryReplyDto>>(new Error("Unauthorized", "Unauthorized")); return Result.Failure<List<StoryReplyDto>>(StoryErrors.Unauthorized);
} }
var replies = new List<StoryReplyDto>(); var replies = new List<StoryReplyDto>();
@@ -1,3 +1,4 @@
using Host.Application.Stories;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -33,11 +34,11 @@ internal sealed class GetStoryViewersQueryHandler : IQueryHandler<GetStoryViewer
if (story == null) if (story == null)
{ {
return Result.Failure<List<StoryViewerDto>>(new Error("Story.NotFound", "Story not found")); return Result.Failure<List<StoryViewerDto>>(StoryErrors.StoryNotFound);
} }
if (story.UserId != request.UserId) if (story.UserId != request.UserId)
{ {
return Result.Failure<List<StoryViewerDto>>(new Error("Unauthorized", "Unauthorized")); return Result.Failure<List<StoryViewerDto>>(StoryErrors.Unauthorized);
} }
var viewerIds = story.Viewers.Select(v => v.UserId).ToList(); var viewerIds = story.Viewers.Select(v => v.UserId).ToList();
@@ -39,7 +39,7 @@ internal sealed class GetUserStoriesQueryHandler : IQueryHandler<GetUserStoriesQ
var user = await _userRepository.GetByIdAsync(request.TargetUserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.TargetUserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<StoryGroupDto>(new Error("User.NotFound", "User not found")); return Result.Failure<StoryGroupDto>(IdentityErrors.UserNotFound);
} }
var result = new StoryGroupDto( var result = new StoryGroupDto(
@@ -0,0 +1,9 @@
using Knot.Shared.Kernel;
namespace Host.Application.Stories;
public static class StoryErrors
{
public static readonly Error Unauthorized = new Error("Unauthorized", "Access denied");
public static readonly Error StoryNotFound = new Error("Story.NotFound", "Story not found");
}
@@ -33,7 +33,7 @@ internal sealed class UploadGroupAvatarCommandHandler : ICommandHandler<UploadGr
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(ChatErrors.ChatNotFound);
} }
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
@@ -67,7 +67,7 @@ internal sealed class CropGroupAvatarCommandHandler : ICommandHandler<CropGroupA
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(ChatErrors.ChatNotFound);
} }
string url; string url;
@@ -117,7 +117,7 @@ internal sealed class RemoveGroupAvatarCommandHandler : ICommandHandler<RemoveGr
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(ChatErrors.ChatNotFound);
} }
chat.UpdateAvatar(null); chat.UpdateAvatar(null);
@@ -25,7 +25,7 @@ internal sealed class ClearChatCommandHandler : ICommandHandler<ClearChatCommand
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<MessageResponse>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<MessageResponse>(ChatErrors.ChatNotFound);
} }
// Currently a placeholder // Currently a placeholder
@@ -36,7 +36,7 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
if (!chat.Members.Any(m => m.UserId == request.UserId)) if (!chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<ChatDto?>(new Error("Chats.Forbidden", "Вы не являетесь участником этого чата.")); return Result.Failure<ChatDto?>(ChatErrors.ChatsForbidden);
} }
var userIdsToFetch = new HashSet<Guid>(); var userIdsToFetch = new HashSet<Guid>();
@@ -1,3 +1,4 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -32,7 +33,7 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler<LeaveOrD
if (!chat.Members.Any(m => m.UserId == request.UserId)) if (!chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<SuccessResponse>(new Error("Unauthorized", "Access denied")); return Result.Failure<SuccessResponse>(ChatErrors.Unauthorized);
} }
if (chat.Type == ChatType.Group) if (chat.Type == ChatType.Group)
@@ -28,7 +28,7 @@ internal sealed class AddMembersCommandHandler : ICommandHandler<AddMembersComma
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(ChatErrors.ChatNotFound);
} }
foreach (var userId in request.UserIdsToAdd) foreach (var userId in request.UserIdsToAdd)
@@ -60,7 +60,7 @@ internal sealed class RemoveMemberCommandHandler : ICommandHandler<RemoveMemberC
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(ChatErrors.ChatNotFound);
} }
chat.RemoveMember(request.UserIdToRemove); chat.RemoveMember(request.UserIdToRemove);
@@ -28,13 +28,13 @@ internal sealed class TogglePinCommandHandler : ICommandHandler<TogglePinCommand
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null) if (chat == null)
{ {
return Result.Failure<TogglePinResponse>(new Error("Chat.NotFound", "Chat not found")); return Result.Failure<TogglePinResponse>(ChatErrors.ChatNotFound);
} }
var member = chat.Members.FirstOrDefault(m => m.UserId == request.UserId); var member = chat.Members.FirstOrDefault(m => m.UserId == request.UserId);
if (member == null) if (member == null)
{ {
return Result.Failure<TogglePinResponse>(new Error("Chat.NotFound", "Member not found")); return Result.Failure<TogglePinResponse>(ChatErrors.ChatNotFound);
} }
member.TogglePin(); member.TogglePin();
@@ -27,7 +27,7 @@ internal sealed class UpdateChatCommandHandler : ICommandHandler<UpdateChatComma
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(ChatErrors.ChatNotFound);
} }
if (request.Name != null) if (request.Name != null)
@@ -32,7 +32,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<List<MessageDetailDto>>(new Error("Chats.Forbidden", "Вы не являетесь участником этого чата.")); return Result.Failure<List<MessageDetailDto>>(ChatErrors.ChatsForbidden);
} }
DateTime? cursorDate = null; DateTime? cursorDate = null;
@@ -41,7 +41,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
cursorDate = parsed.ToUniversalTime(); cursorDate = parsed.ToUniversalTime();
} }
var messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, 100, cancellationToken); var messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, ChatConstants.DefaultMessageQueryLimit, cancellationToken);
var result = new List<MessageDetailDto>(); var result = new List<MessageDetailDto>();
var userIdsToFetch = new HashSet<Guid>(); var userIdsToFetch = new HashSet<Guid>();
@@ -33,10 +33,10 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{ {
return Result.Failure<List<SharedMediaDto>>(new Error("Chats.Forbidden", "Вы не являетесь участником этого чата.")); return Result.Failure<List<SharedMediaDto>>(ChatErrors.ChatsForbidden);
} }
var messages = await _messageRepository.GetChatMessagesAsync(request.ChatId, 300, 0, cancellationToken); var messages = await _messageRepository.GetChatMessagesAsync(request.ChatId, ChatConstants.MaxSharedMediaQueryLimit, 0, cancellationToken);
messages = messages.Where(m => !m.IsDeleted && !m.DeletedByUsers.Contains(request.UserId)).ToList(); messages = messages.Where(m => !m.IsDeleted && !m.DeletedByUsers.Contains(request.UserId)).ToList();
var result = new List<SharedMediaDto>(); var result = new List<SharedMediaDto>();
@@ -57,7 +57,7 @@ public sealed class AddReactionCommandHandler : ICommandHandler<AddReactionComma
if (!success) if (!success)
{ {
_logger.LogWarning("AddReaction: Message not found {MessageId}", request.MessageId); _logger.LogWarning("AddReaction: Message not found {MessageId}", request.MessageId);
return Result.Failure(new Error("Messages.NotFound", "Message not found.")); return Result.Failure(ChatErrors.MessagesNotFound);
} }
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
@@ -44,13 +44,13 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat is null) if (chat is null)
{ {
return Result.Failure<Guid>(new Error("Chats.NotFound", "Чат не найден.")); return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
} }
// 2. Проверяем, является ли отправитель участником // 2. Проверяем, является ли отправитель участником
if (!chat.Members.Any(m => m.UserId == request.SenderId)) if (!chat.Members.Any(m => m.UserId == request.SenderId))
{ {
return Result.Failure<Guid>(new Error("Chats.Forbidden", "Вы не являетесь участником этого чата.")); return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
} }
// 3. Создаем сообщение // 3. Создаем сообщение
@@ -1,4 +1,5 @@
using System; using Knot.Modules.Chats.Domain;
using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -27,13 +28,13 @@ internal sealed class UploadFileCommandHandler : ICommandHandler<UploadFileComma
{ {
if (request.Length == 0) if (request.Length == 0)
{ {
return Result.Failure<UploadFileResponseDto>(new Error("File.Empty", "No file uploaded")); return Result.Failure<UploadFileResponseDto>(ChatErrors.FileEmpty);
} }
var maxMb = _settingsService.Current.MaxFileSizeMb; var maxMb = _settingsService.Current.MaxFileSizeMb;
if (request.Length > maxMb * 1024 * 1024) if (request.Length > maxMb * 1024 * 1024)
{ {
return Result.Failure<UploadFileResponseDto>(new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB.")); return Result.Failure<UploadFileResponseDto>(ChatErrors.FileTooLarge(maxMb));
} }
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
@@ -1,3 +1,4 @@
using Knot.Modules.Chats.Domain;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@@ -28,12 +29,12 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
{ {
if (request.FileStream == null || request.FileStream.Length == 0) if (request.FileStream == null || request.FileStream.Length == 0)
{ {
return Result.Failure<AnalyzeImportResponseDto>(new Error("File.Empty", "No file uploaded")); return Result.Failure<AnalyzeImportResponseDto>(ChatErrors.FileEmpty);
} }
if (!request.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) if (!request.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{ {
return Result.Failure<AnalyzeImportResponseDto>(new Error("File.InvalidExtension", "Must be a ZIP archive")); return Result.Failure<AnalyzeImportResponseDto>(ChatErrors.FileInvalidExtension);
} }
var token = Guid.NewGuid(); var token = Guid.NewGuid();
@@ -56,12 +56,12 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
if (!TelegramImportState.TempZips.TryGetValue(request.Token, out var tempPath)) if (!TelegramImportState.TempZips.TryGetValue(request.Token, out var tempPath))
{ {
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.Expired", "Session not found or expired")); return Result.Failure<ExecuteImportResponseDto>(ChatErrors.ImportExpired);
} }
if (!System.IO.File.Exists(tempPath)) if (!System.IO.File.Exists(tempPath))
{ {
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.Missing", "ZIP file lost")); return Result.Failure<ExecuteImportResponseDto>(ChatErrors.ImportMissing);
} }
var myId = request.CurrentUserId; var myId = request.CurrentUserId;
@@ -95,7 +95,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
var res = await _sender.Send(command, cancellationToken); var res = await _sender.Send(command, cancellationToken);
if (res.IsFailure) if (res.IsFailure)
{ {
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.CreateChatFailed", res.Error.Description ?? res.Error.Code)); return Result.Failure<ExecuteImportResponseDto>(ChatErrors.ImportCreateChatFailed(res.Error.Description ?? res.Error.Code));
} }
chatId = res.Value; chatId = res.Value;
@@ -107,7 +107,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
var res = await _sender.Send(command, cancellationToken); var res = await _sender.Send(command, cancellationToken);
if (res.IsFailure) if (res.IsFailure)
{ {
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.CreateChatFailed", res.Error.Description ?? res.Error.Code)); return Result.Failure<ExecuteImportResponseDto>(ChatErrors.ImportCreateChatFailed(res.Error.Description ?? res.Error.Code));
} }
chatId = res.Value; chatId = res.Value;
@@ -0,0 +1,9 @@
namespace Knot.Modules.Chats.Domain;
public static class ChatConstants
{
public const int DefaultMessageQueryLimit = 100;
public const int MaxSharedMediaQueryLimit = 300;
public const int SearchMessagesLimit = 50;
public const int MaxFileUploadSizeMb = 50;
}
@@ -0,0 +1,19 @@
using Knot.Shared.Kernel;
namespace Knot.Modules.Chats.Domain;
public static class ChatErrors
{
public static readonly Error FileEmpty = new Error("File.Empty", "No file uploaded");
public static readonly Error FileInvalidExtension = new Error("File.InvalidExtension", "Must be a ZIP archive");
public static readonly Error ImportExpired = new Error("Import.Expired", "Session not found or expired");
public static readonly Error ImportMissing = new Error("Import.Missing", "ZIP file lost");
public static readonly Error ChatNotFound = new Error("Chat.NotFound", "Chat not found or access denied");
public static readonly Error ChatsForbidden = new Error("Chats.Forbidden", "Вы не являетесь участником этого чата.");
public static readonly Error MessagesNotFound = new Error("Messages.NotFound", "Message not found.");
public static readonly Error ChatsNotFound = new Error("Chats.NotFound", "Чат не найден.");
public static readonly Error Unauthorized = new Error("Chats.Unauthorized", "Access denied");
public static Error ImportCreateChatFailed(string msg) => new Error("Import.CreateChatFailed", msg);
public static Error FileTooLarge(int maxMb) => new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB.");
}
@@ -74,7 +74,7 @@ public sealed class MessageRepository : IMessageRepository
return await q.Where(m => m.Content != null && m.Content.Contains(query)) return await q.Where(m => m.Content != null && m.Content.Contains(query))
.OrderByDescending(m => m.CreatedAt) .OrderByDescending(m => m.CreatedAt)
.Take(50) .Take(ChatConstants.SearchMessagesLimit)
.Include(m => m.Media) .Include(m => m.Media)
.Include(m => m.ReadBy) .Include(m => m.ReadBy)
.Include(m => m.Reactions) .Include(m => m.Reactions)
@@ -1,3 +1,4 @@
using Knot.Modules.Identity.Domain;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -24,7 +25,7 @@ internal sealed class AcceptFriendRequestCommandHandler : ICommandHandler<Accept
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken); var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || friendship.FriendId != request.UserId) if (friendship == null || friendship.FriendId != request.UserId)
{ {
return Result.Failure<Guid>(new Error("Friends.NotFound", "Request not found")); return Result.Failure<Guid>(IdentityErrors.FriendsNotFound);
} }
friendship.Accept(); friendship.Accept();
@@ -1,3 +1,4 @@
using Knot.Modules.Identity.Domain;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -24,7 +25,7 @@ internal sealed class DeclineFriendRequestCommandHandler : ICommandHandler<Decli
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken); var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || friendship.FriendId != request.UserId) if (friendship == null || friendship.FriendId != request.UserId)
{ {
return Result.Failure(new Error("Friends.NotFound", "Request not found")); return Result.Failure(IdentityErrors.FriendsNotFound);
} }
friendship.Decline(); friendship.Decline();
@@ -1,3 +1,4 @@
using Knot.Modules.Identity.Domain;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -24,7 +25,7 @@ internal sealed class RemoveFriendCommandHandler : ICommandHandler<RemoveFriendC
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken); var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || (friendship.UserId != request.UserId && friendship.FriendId != request.UserId)) if (friendship == null || (friendship.UserId != request.UserId && friendship.FriendId != request.UserId))
{ {
return Result.Failure(new Error("Friends.NotFound", "Friendship not found")); return Result.Failure(IdentityErrors.FriendsNotFound);
} }
_context.Friendships.Remove(friendship); _context.Friendships.Remove(friendship);
@@ -25,7 +25,7 @@ internal sealed class SendFriendRequestCommandHandler : ICommandHandler<SendFrie
{ {
if (request.UserId == request.FriendId) if (request.UserId == request.FriendId)
{ {
return Result.Failure(new Error("Friends.Self", "Cannot add yourself")); return Result.Failure(IdentityErrors.FriendsSelf);
} }
var existing = await _context.Friendships var existing = await _context.Friendships
@@ -34,7 +34,7 @@ internal sealed class SendFriendRequestCommandHandler : ICommandHandler<SendFrie
if (existing != null) if (existing != null)
{ {
return Result.Failure(new Error("Friends.Exists", "Friendship already exists")); return Result.Failure(IdentityErrors.FriendsExists);
} }
var friendship = Friendship.Create(request.UserId, request.FriendId); var friendship = Friendship.Create(request.UserId, request.FriendId);
@@ -27,7 +27,7 @@ internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarComma
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
} }
string avatarUrl; string avatarUrl;
@@ -22,7 +22,7 @@ internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarC
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
} }
user.UpdateAvatar(null); user.UpdateAvatar(null);
@@ -25,7 +25,7 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
} }
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
@@ -21,7 +21,7 @@ internal sealed class GetMeQueryHandler : IQueryHandler<GetMeQuery, AuthResponse
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<AuthResponseDto>(new Error("User.NotFound", "User not found")); return Result.Failure<AuthResponseDto>(IdentityErrors.UserNotFound);
} }
var response = new AuthResponseDto( var response = new AuthResponseDto(
@@ -20,7 +20,7 @@ internal sealed class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserProf
var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken); var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
} }
var dto = new UserProfileDto( var dto = new UserProfileDto(
@@ -28,7 +28,7 @@ public sealed class LoginUserCommandHandler : ICommandHandler<LoginUserCommand,
if (user is null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) if (user is null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
{ {
return Result.Failure<AuthResponseDto>(new Error("Identity.InvalidCredentials", "Неверное имя пользователя или пароль.")); return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityInvalidCredentials);
} }
string token = _tokenProvider.Generate(user); string token = _tokenProvider.Generate(user);
@@ -43,13 +43,13 @@ public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCom
{ {
if (!_settings.Current.EnableRegistration) if (!_settings.Current.EnableRegistration)
{ {
return Result.Failure<AuthResponseDto>(new Error("Identity.RegistrationDisabled", "Registration is disabled by the administrator.")); return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityRegistrationDisabled);
} }
// 1. Проверка уникальности username // 1. Проверка уникальности username
if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken)) if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken))
{ {
return Result.Failure<AuthResponseDto>(new Error("Identity.UsernameNotUnique", "Это имя пользователя уже занято.")); return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityUsernameNotUnique);
} }
// 2. Хеширование пароля (здесь будет вызов сервиса, пока заглушка) // 2. Хеширование пароля (здесь будет вызов сервиса, пока заглушка)
@@ -22,7 +22,7 @@ internal sealed class UpdateProfileCommandHandler : ICommandHandler<UpdateProfil
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
} }
user.UpdateProfile(request.DisplayName ?? user.DisplayName, request.Bio, request.Birthday); user.UpdateProfile(request.DisplayName ?? user.DisplayName, request.Bio, request.Birthday);
@@ -22,7 +22,7 @@ internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSetti
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) if (user == null)
{ {
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
} }
user.UpdateSettings(request.HideStoryViews ?? user.HideStoryViews); user.UpdateSettings(request.HideStoryViews ?? user.HideStoryViews);
@@ -0,0 +1,14 @@
using Knot.Shared.Kernel;
namespace Knot.Modules.Identity.Domain;
public static class IdentityErrors
{
public static readonly Error FriendsNotFound = new Error("Friends.NotFound", "Friendship not found");
public static readonly Error FriendsSelf = new Error("Friends.Self", "Cannot add yourself");
public static readonly Error FriendsExists = new Error("Friends.Exists", "Friendship already exists");
public static readonly Error UserNotFound = new Error("User.NotFound", "User not found");
public static readonly Error IdentityInvalidCredentials = new Error("Identity.InvalidCredentials", "Неверное имя пользователя или пароль.");
public static readonly Error IdentityRegistrationDisabled = new Error("Identity.RegistrationDisabled", "Registration is disabled by the administrator.");
public static readonly Error IdentityUsernameNotUnique = new Error("Identity.UsernameNotUnique", "Это имя пользователя уже занято.");
}
@@ -0,0 +1,7 @@
namespace Knot.Shared.Kernel;
public static class DomainErrors
{
public static readonly Error InvalidPassword = new Error("Admin.InvalidPassword", "Пароль не должен быть пустым.");
public static readonly Error RequestInvalid = new Error("Request.Invalid", "Invalid federation request");
}