From cb93ff72401accdbffe3aeb4eb7741e3f3282f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Mon, 30 Mar 2026 17:48:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BA=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/Abstractions/AggregateRoot.cs | 22 +++ .../Contracts/Admin/Abstractions/Entity.cs | 43 ++++++ .../Contracts/Admin/Abstractions/ICommand.cs | 15 ++ .../Admin/Abstractions/IDomainEvent.cs | 8 + .../Contracts/Admin/Abstractions/IQuery.cs | 13 ++ .../Admin/Abstractions/MessageResponse.cs | 3 + .../Contracts/Admin/Abstractions/Result.cs | 63 ++++++++ .../Admin/Abstractions/SuccessResponse.cs | 3 + .../Admin/Knot.Contracts.Admin.csproj | 4 - .../Persistence/IAuthDbContext.cs | 13 ++ .../Persistence/IChatsDbContext.cs | 18 +-- .../Application/Abstractions/IKlipyClient.cs | 9 ++ .../Abstractions/IMessageQueryService.cs | 17 ++ .../Abstractions/IUserStatsService.cs | 12 ++ .../Abstractions/IStatisticsService.cs | 37 +++++ .../Abstractions/IFileStorageService.cs | 25 +++ .../Persistence/IStoryCollection.cs | 20 +++ .../Admin/Commands/BanUser/BanUserCommand.cs | 16 +- .../Commands/BanUser/UnbanUserCommand.cs | 16 +- .../Commands/CleanRun/CleanRunCommand.cs | 56 +++---- .../ResetUserPasswordCommand.cs | 7 +- .../TestKlipy/TestKlipyConnectionCommand.cs | 10 +- .../UpdateSettings/UpdateSettingsCommand.cs | 16 +- .../Admin/DTOs/CleanDryRunResult.cs | 9 ++ .../Queries/CleanDryRun/CleanDryRunQuery.cs | 145 ++++++------------ .../GetDashboardStatsQuery.cs | 6 +- .../Queries/GetSettings/GetSettingsQuery.cs | 6 +- .../GetUserDetails/GetUserDetailsQuery.cs | 111 +++++--------- .../Queries/SearchUsers/SearchUsersQuery.cs | 35 ++--- .../SystemSettingsUpdatedDomainEvent.cs | 3 +- .../Modules/Admin/Knot.Modules.Admin.csproj | 14 +- .../Presentation/Endpoints/AdminEndpoints.cs | 35 ++--- .../src/Modules/Auth/DependencyInjection.cs | 10 +- .../Persistence/AuthDbContext.cs | 18 ++- .../Conversations/DependencyInjection.cs | 2 + .../Persistence/ConversationsDbContext.cs | 38 ++--- .../Infrastructure/External/KlipyClient.cs | 6 +- .../Modules/Messaging/DependencyInjection.cs | 5 +- .../Persistence/Mongo/UserStatsService.cs | 31 +++- .../Modules/Stories/DependencyInjection.cs | 11 +- .../Persistence/Mongo/StoryCollection.cs | 33 ++++ 41 files changed, 620 insertions(+), 344 deletions(-) create mode 100644 backend/src/Contracts/Admin/Abstractions/AggregateRoot.cs create mode 100644 backend/src/Contracts/Admin/Abstractions/Entity.cs create mode 100644 backend/src/Contracts/Admin/Abstractions/ICommand.cs create mode 100644 backend/src/Contracts/Admin/Abstractions/IDomainEvent.cs create mode 100644 backend/src/Contracts/Admin/Abstractions/IQuery.cs create mode 100644 backend/src/Contracts/Admin/Abstractions/MessageResponse.cs create mode 100644 backend/src/Contracts/Admin/Abstractions/Result.cs create mode 100644 backend/src/Contracts/Admin/Abstractions/SuccessResponse.cs create mode 100644 backend/src/Contracts/Auth/Infrastructure/Persistence/IAuthDbContext.cs create mode 100644 backend/src/Contracts/Klipy/Application/Abstractions/IKlipyClient.cs create mode 100644 backend/src/Contracts/Messaging/Application/Abstractions/IMessageQueryService.cs create mode 100644 backend/src/Contracts/Messaging/Application/Abstractions/IUserStatsService.cs create mode 100644 backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs create mode 100644 backend/src/Contracts/Storage/Abstractions/IFileStorageService.cs create mode 100644 backend/src/Contracts/Stories/Infrastructure/Persistence/IStoryCollection.cs create mode 100644 backend/src/Modules/Admin/Application/Admin/DTOs/CleanDryRunResult.cs create mode 100644 backend/src/Modules/Stories/Infrastructure/Persistence/Mongo/StoryCollection.cs diff --git a/backend/src/Contracts/Admin/Abstractions/AggregateRoot.cs b/backend/src/Contracts/Admin/Abstractions/AggregateRoot.cs new file mode 100644 index 0000000..1c30236 --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/AggregateRoot.cs @@ -0,0 +1,22 @@ +namespace Knot.Contracts.Admin.Abstractions; + +/// +/// Базовый класс для агрегатов - сущностей с бизнес-логикой и доменными событиями. +/// +public abstract class AggregateRoot : Entity where TId : notnull +{ + private readonly List _domainEvents = new(); + public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); + + protected AggregateRoot(TId id) : base(id) { } + + protected void RaiseDomainEvent(IDomainEvent domainEvent) + { + _domainEvents.Add(domainEvent); + } + + public void ClearDomainEvents() + { + _domainEvents.Clear(); + } +} diff --git a/backend/src/Contracts/Admin/Abstractions/Entity.cs b/backend/src/Contracts/Admin/Abstractions/Entity.cs new file mode 100644 index 0000000..47c46b6 --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/Entity.cs @@ -0,0 +1,43 @@ +namespace Knot.Contracts.Admin.Abstractions; + +/// +/// Базовый класс для сущностей с уникальным идентификатором. +/// +public abstract class Entity where TId : notnull +{ + public TId Id { get; protected set; } + + protected Entity(TId id) + { + Id = id; + } + + public override bool Equals(object? obj) + { + if (obj is not Entity other) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (GetType() != other.GetType()) + return false; + + return Id.Equals(other.Id); + } + + public override int GetHashCode() => Id.GetHashCode(); + + public static bool operator ==(Entity? a, Entity? b) + { + if (a is null && b is null) + return true; + + if (a is null || b is null) + return false; + + return a.Equals(b); + } + + public static bool operator !=(Entity? a, Entity? b) => !(a == b); +} diff --git a/backend/src/Contracts/Admin/Abstractions/ICommand.cs b/backend/src/Contracts/Admin/Abstractions/ICommand.cs new file mode 100644 index 0000000..eeb8e1d --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/ICommand.cs @@ -0,0 +1,15 @@ +using MediatR; + +namespace Knot.Contracts.Admin.Abstractions; + +public interface ICommand : IRequest { } + +public interface ICommand : IRequest> { } + +public interface ICommandHandler : IRequestHandler + where TCommand : ICommand +{ } + +public interface ICommandHandler : IRequestHandler> + where TCommand : ICommand +{ } diff --git a/backend/src/Contracts/Admin/Abstractions/IDomainEvent.cs b/backend/src/Contracts/Admin/Abstractions/IDomainEvent.cs new file mode 100644 index 0000000..ceec674 --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/IDomainEvent.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Knot.Contracts.Admin.Abstractions; + +/// +/// Интерфейс для доменных событий. +/// +public interface IDomainEvent : INotification { } diff --git a/backend/src/Contracts/Admin/Abstractions/IQuery.cs b/backend/src/Contracts/Admin/Abstractions/IQuery.cs new file mode 100644 index 0000000..81ed2f3 --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/IQuery.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Knot.Contracts.Admin.Abstractions; + +public interface IQuery : IRequest> +{ +} + +public interface IQueryHandler + : IRequestHandler> + where TQuery : IQuery +{ +} diff --git a/backend/src/Contracts/Admin/Abstractions/MessageResponse.cs b/backend/src/Contracts/Admin/Abstractions/MessageResponse.cs new file mode 100644 index 0000000..e532e8f --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/MessageResponse.cs @@ -0,0 +1,3 @@ +namespace Knot.Contracts.Admin.Abstractions; + +public record MessageResponse(string Message); diff --git a/backend/src/Contracts/Admin/Abstractions/Result.cs b/backend/src/Contracts/Admin/Abstractions/Result.cs new file mode 100644 index 0000000..e0a4a6c --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/Result.cs @@ -0,0 +1,63 @@ +namespace Knot.Contracts.Admin.Abstractions; + +/// +/// Представляет ошибку в доменной логике. +/// +public sealed record Error(string Code, string Description) +{ + public static readonly Error None = new(string.Empty, string.Empty); + public static Error NotFound(string code, string description) => new(code, description); +} + +/// +/// Общая обертка для результата операции. Позволяет избегать использования исключений для управления потоком. +/// +public class Result +{ + public bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + public Error Error { get; } + + protected Result(bool isSuccess, Error error) + { + if (isSuccess && error != Error.None) + { + throw new InvalidOperationException(); + } + + if (!isSuccess && error == Error.None) + { + throw new InvalidOperationException(); + } + + IsSuccess = isSuccess; + Error = error; + } + + public static Result Success() => new(true, Error.None); + public static Result Failure(Error error) => new(false, error); + + public static Result Success(TValue value) => Result.Success(value); + public static Result Failure(Error error) => Result.Failure(error); +} + +/// +/// Результ операции, содержащий значение. +/// +public class Result : Result +{ + private readonly TValue? _value; + + protected internal Result(TValue? value, bool isSuccess, Error error) + : base(isSuccess, error) + { + _value = value; + } + + public TValue Value => IsSuccess + ? _value! + : throw new InvalidOperationException("Нельзя получить значение ошибочного результата."); + + public static Result Success(TValue value) => new(value, true, Error.None); + public new static Result Failure(Error error) => new(default, false, error); +} diff --git a/backend/src/Contracts/Admin/Abstractions/SuccessResponse.cs b/backend/src/Contracts/Admin/Abstractions/SuccessResponse.cs new file mode 100644 index 0000000..9e256a3 --- /dev/null +++ b/backend/src/Contracts/Admin/Abstractions/SuccessResponse.cs @@ -0,0 +1,3 @@ +namespace Knot.Contracts.Admin.Abstractions; + +public record SuccessResponse(bool Success); diff --git a/backend/src/Contracts/Admin/Knot.Contracts.Admin.csproj b/backend/src/Contracts/Admin/Knot.Contracts.Admin.csproj index 4bd9c60..ee7380a 100644 --- a/backend/src/Contracts/Admin/Knot.Contracts.Admin.csproj +++ b/backend/src/Contracts/Admin/Knot.Contracts.Admin.csproj @@ -6,10 +6,6 @@ enable - - - - diff --git a/backend/src/Contracts/Auth/Infrastructure/Persistence/IAuthDbContext.cs b/backend/src/Contracts/Auth/Infrastructure/Persistence/IAuthDbContext.cs new file mode 100644 index 0000000..cd456d2 --- /dev/null +++ b/backend/src/Contracts/Auth/Infrastructure/Persistence/IAuthDbContext.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Knot.Contracts.Auth.Domain; + +namespace Knot.Contracts.Auth.Infrastructure.Persistence; + +public interface IAuthDbContext +{ + IQueryable Users { get; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/backend/src/Contracts/Conversations/Infrastructure/Persistence/IChatsDbContext.cs b/backend/src/Contracts/Conversations/Infrastructure/Persistence/IChatsDbContext.cs index ccfba8b..4e1db8d 100644 --- a/backend/src/Contracts/Conversations/Infrastructure/Persistence/IChatsDbContext.cs +++ b/backend/src/Contracts/Conversations/Infrastructure/Persistence/IChatsDbContext.cs @@ -1,20 +1,18 @@ using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Knot.Contracts.Conversations.Application.Abstractions; -using Knot.Shared.Kernel; +using Knot.Contracts.Conversations.Domain; namespace Knot.Contracts.Conversations.Infrastructure.Persistence; -public interface IChatsDbContext : IChatsUnitOfWork +public interface IChatsDbContext { IQueryable Chats { get; } } -public class Chat : AggregateRoot +/// +/// Упрощенная проекция чата для запросов (не доменная сущность). +/// +public class Chat { - public Chat() : base(Guid.NewGuid()) { } - - + public Guid Id { get; set; } public string? Avatar { get; set; } -} \ No newline at end of file +} diff --git a/backend/src/Contracts/Klipy/Application/Abstractions/IKlipyClient.cs b/backend/src/Contracts/Klipy/Application/Abstractions/IKlipyClient.cs new file mode 100644 index 0000000..67ad5a1 --- /dev/null +++ b/backend/src/Contracts/Klipy/Application/Abstractions/IKlipyClient.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Contracts.Klipy.Application.Abstractions; + +public interface IKlipyClient +{ + Task TestConnectionAsync(string apiKey, string appName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/backend/src/Contracts/Messaging/Application/Abstractions/IMessageQueryService.cs b/backend/src/Contracts/Messaging/Application/Abstractions/IMessageQueryService.cs new file mode 100644 index 0000000..6dcbeaa --- /dev/null +++ b/backend/src/Contracts/Messaging/Application/Abstractions/IMessageQueryService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Contracts.Messaging.Application.Abstractions; + +public interface IMessageQueryService +{ + Task> GetAllMessagesAsync(CancellationToken cancellationToken); + Task> GetOrphanedMessagesAsync(HashSet activeChatIds, CancellationToken cancellationToken); + Task DeleteMessagesAsync(List messageIds, CancellationToken cancellationToken); +} + +public record MessageInfo(Guid Id, Guid ChatId, bool IsDeleted, string? MediaUrl, List? Media); + +public record MediaInfo(string Url); \ No newline at end of file diff --git a/backend/src/Contracts/Messaging/Application/Abstractions/IUserStatsService.cs b/backend/src/Contracts/Messaging/Application/Abstractions/IUserStatsService.cs new file mode 100644 index 0000000..64694b5 --- /dev/null +++ b/backend/src/Contracts/Messaging/Application/Abstractions/IUserStatsService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Contracts.Messaging.Application.Abstractions; + +public interface IUserStatsService +{ + Task> GetStatsForUsersAsync(List userIds, CancellationToken cancellationToken = default); +} + +public record UserStats(int MessageCount, long StorageSize); \ No newline at end of file diff --git a/backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs b/backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs new file mode 100644 index 0000000..da97d46 --- /dev/null +++ b/backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Contracts.Settings.Abstractions; + +public interface IStatisticsService +{ + Task GetDashboardStatsAsync(CancellationToken cancellationToken = default); +} + +public class DashboardStatsDto +{ + public long StorageUsedBytes { get; set; } + public long StorageLimitBytes { get; set; } + public long OnlineUsers { get; set; } + public long OfflineUsers { get; set; } + public long TotalUsers { get; set; } + public List ActivityTimeline { get; set; } = new(); + public List TopUsersByMessages { get; set; } = new(); + public List TopUsersByStorage { get; set; } = new(); +} + +public class ActivityStatDto +{ + public DateTime Date { get; set; } + public long Messages { get; set; } + public long FilesSize { get; set; } +} + +public class TopUserDto +{ + public Guid UserId { get; set; } + public string Username { get; set; } = string.Empty; + public long Value { get; set; } // messages count or bytes +} diff --git a/backend/src/Contracts/Storage/Abstractions/IFileStorageService.cs b/backend/src/Contracts/Storage/Abstractions/IFileStorageService.cs new file mode 100644 index 0000000..414937b --- /dev/null +++ b/backend/src/Contracts/Storage/Abstractions/IFileStorageService.cs @@ -0,0 +1,25 @@ +using System.IO; +using System.Threading.Tasks; + +namespace Knot.Contracts.Storage.Abstractions; + +public interface IFileStorageService +{ + // Загружает поток файла и возвращает его уникальный идентификатор (SHA256 хеш или GUID). + Task UploadFileAsync(Stream stream, string fileName, string contentType); + + // Скачивает файл и возвращает его расшифрованный поток и тип содержимого. + Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId); + + // Удаляет файл из хранилища. + Task DeleteFileAsync(string fileId); + + // Получает список всех файлов в хранилище с их размерами. + Task> ListFilesAsync(); + + // Получает размер конкретного файла + Task GetFileSizeAsync(string fileId, CancellationToken ct = default); + + // Получает размеры списка файлов + Task> GetFileSizesAsync(IEnumerable fileIds, CancellationToken ct = default); +} diff --git a/backend/src/Contracts/Stories/Infrastructure/Persistence/IStoryCollection.cs b/backend/src/Contracts/Stories/Infrastructure/Persistence/IStoryCollection.cs new file mode 100644 index 0000000..b9341c9 --- /dev/null +++ b/backend/src/Contracts/Stories/Infrastructure/Persistence/IStoryCollection.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Contracts.Stories.Infrastructure.Persistence; + +public interface IStoryCollection +{ + Task> GetAllAsync(CancellationToken cancellationToken); +} + +public class StoryContract +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string? MediaUrl { get; set; } + public string? MediaType { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? ExpiresAt { get; set; } +} \ No newline at end of file diff --git a/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/BanUserCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/BanUserCommand.cs index 16fef17..bf7ea30 100644 --- a/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/BanUserCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/BanUserCommand.cs @@ -1,16 +1,16 @@ -using Knot.Contracts.Auth.Domain; -using Knot.Contracts.Auth.Application.Abstractions; -using Knot.Shared.Kernel; using System; using System.Threading; using System.Threading.Tasks; +using Knot.Contracts.Admin.Abstractions; +using Knot.Contracts.Auth.Application.Abstractions; +using Knot.Contracts.Auth.Domain; using MediatR; namespace Knot.Modules.Admin.Application.Admin.Commands; -public record BanUserCommand(Guid UserId) : ICommand; +public record BanUserCommand(Guid UserId) : ICommand; -internal sealed class BanUserCommandHandler : ICommandHandler +internal sealed class BanUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly IAuthUnitOfWork _unitOfWork; @@ -21,16 +21,16 @@ internal sealed class BanUserCommandHandler : ICommandHandler> Handle(BanUserCommand request, CancellationToken cancellationToken) + public async Task> Handle(BanUserCommand request, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); - if (user == null) return Result.Failure(Error.NotFound("User.NotFound", "User not found")); + if (user == null) return Result.Failure(Error.NotFound("User.NotFound", "User not found")); user.IsBanned = true; user.BannedUntil = null; // Permanent ban await _userRepository.UpdateAsync(user, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); - return Result.Success(Result.Success()); + return Result.Success(new SuccessResponse(true)); } } diff --git a/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/UnbanUserCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/UnbanUserCommand.cs index 48ef80e..4e49465 100644 --- a/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/UnbanUserCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/BanUser/UnbanUserCommand.cs @@ -1,16 +1,16 @@ -using Knot.Contracts.Auth.Domain; -using Knot.Contracts.Auth.Application.Abstractions; -using Knot.Shared.Kernel; using System; using System.Threading; using System.Threading.Tasks; +using Knot.Contracts.Admin.Abstractions; +using Knot.Contracts.Auth.Application.Abstractions; +using Knot.Contracts.Auth.Domain; using MediatR; namespace Knot.Modules.Admin.Application.Admin.Commands; -public record UnbanUserCommand(Guid UserId) : ICommand; +public record UnbanUserCommand(Guid UserId) : ICommand; -internal sealed class UnbanUserCommandHandler : ICommandHandler +internal sealed class UnbanUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly IAuthUnitOfWork _unitOfWork; @@ -21,16 +21,16 @@ internal sealed class UnbanUserCommandHandler : ICommandHandler> Handle(UnbanUserCommand request, CancellationToken cancellationToken) + public async Task> Handle(UnbanUserCommand request, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); - if (user == null) return Result.Failure(Error.NotFound("User.NotFound", "User not found")); + if (user == null) return Result.Failure(Error.NotFound("User.NotFound", "User not found")); user.IsBanned = false; user.BannedUntil = null; await _userRepository.UpdateAsync(user, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); - return Result.Success(Result.Success()); + return Result.Success(new SuccessResponse(true)); } } diff --git a/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs index 41c05ba..01f1db6 100644 --- a/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs @@ -3,17 +3,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Knot.Contracts.Auth.Application.Abstractions; -using Knot.Contracts.Messaging.Domain; +using Knot.Contracts.Admin.Abstractions; +using Knot.Contracts.Auth.Infrastructure.Persistence; +using Knot.Contracts.Conversations.Domain; +using Knot.Contracts.Conversations.Infrastructure.Persistence; +using Knot.Contracts.Messaging.Application.Abstractions; +using Knot.Contracts.Storage.Abstractions; +using Knot.Contracts.Stories.Infrastructure.Persistence; using Knot.Modules.Admin.Application.Admin.DTOs; -using Knot.Modules.Auth.Infrastructure.Persistence; -using Knot.Modules.Conversations.Infrastructure.Persistence; -using Knot.Modules.Stories.Domain; -using Knot.Shared.Kernel; -using Knot.Shared.Kernel.Storage; using MediatR; using Microsoft.EntityFrameworkCore; -using MongoDB.Driver; namespace Knot.Modules.Admin.Application.Admin.Commands; @@ -21,17 +20,22 @@ public record CleanRunCommand() : ICommand; internal sealed class CleanRunCommandHandler : ICommandHandler { - private readonly ChatsDbContext _chatsDbContext; - private readonly IMongoCollection _messages; - private readonly IMongoCollection _stories; + private readonly Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext _chatsDbContext; + private readonly Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService _messageQueryService; + private readonly Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection _storyCollection; private readonly IFileStorageService _fileStorage; - private readonly AuthDbContext _identityDb; + private readonly Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext _identityDb; - public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb, IFileStorageService fileStorage, AuthDbContext identityDb) + public CleanRunCommandHandler( + Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext chatsDbContext, + Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService messageQueryService, + Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection storyCollection, + IFileStorageService fileStorage, + Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext identityDb) { _chatsDbContext = chatsDbContext; - _messages = mongoDb.GetCollection("messages"); - _stories = mongoDb.GetCollection("stories"); + _messageQueryService = messageQueryService; + _storyCollection = storyCollection; _fileStorage = fileStorage; _identityDb = identityDb; } @@ -40,30 +44,27 @@ internal sealed class CleanRunCommandHandler : ICommandHandler c.Id).ToHashSet(); - var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken); - - var orphanMessages = allMessages - .Where(m => !activeChatIds.Contains(m.ChatId) || m.IsDeleted) - .ToList(); + var allMessages = await _messageQueryService.GetAllMessagesAsync(cancellationToken); + var orphanMessages = await _messageQueryService.GetOrphanedMessagesAsync(activeChatIds, cancellationToken); var keptMessages = allMessages - .Where(m => activeChatIds.Contains(m.ChatId) && !m.IsDeleted) + .Where(m => !orphanMessages.Any(om => om.Id == m.Id)) .ToList(); var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList(); - var allUsers = await _identityDb.Users.AsNoTracking().ToListAsync(cancellationToken); + var allUsers = await _identityDb.Users.ToListAsync(cancellationToken); - var allStories = await _stories.Find(_ => true).ToListAsync(cancellationToken); + var allStories = await _storyCollection.GetAllAsync(cancellationToken); var validUrls = new HashSet(); - var activeMessageUrls = keptMessages.OfType() + var activeMessageUrls = keptMessages .Where(m => m.Media != null) - .SelectMany(m => m.Media) + .SelectMany(m => m.Media!) .Select(me => me.Url) .Where(u => !string.IsNullOrEmpty(u)); @@ -100,8 +101,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler m.Id).ToList(); - var filter = Builders.Filter.In(m => m.Id, orphanIds); - await _messages.DeleteManyAsync(filter, cancellationToken); + await _messageQueryService.DeleteMessagesAsync(orphanIds, cancellationToken); } return Result.Success(new MessageResponse("Cleanup completed successfully")); diff --git a/backend/src/Modules/Admin/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs index 9a2ff3d..14c7e23 100644 --- a/backend/src/Modules/Admin/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs @@ -6,7 +6,7 @@ using System; using System.Threading; using System.Threading.Tasks; using MediatR; -using Knot.Shared.Kernel; +using Knot.Contracts.Admin.Abstractions; namespace Knot.Modules.Admin.Application.Admin.Commands; @@ -28,10 +28,10 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler(AuthErrors.UserNotFound); + return Result.Failure(Error.NotFound("Auth.UserNotFound", "User not found")); } - if (string.IsNullOrWhiteSpace(request.NewPassword)) + if (string.IsNullOrWhiteSpace(request.NewPassword)) return Result.Failure(new Error("Admin.InvalidPassword", "Invalid password")); var hash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); @@ -42,3 +42,4 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler { private readonly ISettingsService _settingsService; - private readonly IKlipyClient _klipyClient; + private readonly Knot.Contracts.Klipy.Application.Abstractions.IKlipyClient _klipyClient; - public TestKlipyConnectionCommandHandler(ISettingsService settingsService, IKlipyClient klipyClient) + public TestKlipyConnectionCommandHandler(ISettingsService settingsService, Knot.Contracts.Klipy.Application.Abstractions.IKlipyClient klipyClient) { _settingsService = settingsService; _klipyClient = klipyClient; diff --git a/backend/src/Modules/Admin/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs index d6e3a06..8987b02 100644 --- a/backend/src/Modules/Admin/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs @@ -1,12 +1,12 @@ -using Knot.Modules.Admin.Application.Admin.DTOs; using System; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; -using MediatR; -using Knot.Shared.Kernel; +using Knot.Contracts.Admin.Abstractions; using Knot.Contracts.Settings.Application.Abstractions; using Knot.Contracts.Settings.Application.DTOs; -using System.Security.Cryptography; +using Knot.Modules.Admin.Application.Admin.DTOs; +using MediatR; namespace Knot.Modules.Admin.Application.Admin.Commands; @@ -26,19 +26,19 @@ internal sealed class UpdateSettingsCommandHandler : ICommandHandler> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken) { // Если федерация включена, но ключи еще не сгенерированы — создаем их - if (request.Settings.Federation.Enabled && - (string.IsNullOrEmpty(request.Settings.Federation.PrivateKey) || + if (request.Settings.Federation.Enabled && + (string.IsNullOrEmpty(request.Settings.Federation.PrivateKey) || string.IsNullOrEmpty(request.Settings.Federation.PublicKey))) { using var rsa = RSA.Create(2048); - + // Экспорт в формате PKCS#8 (Private) и PKCS#1 (Public) в Base64 PEM-строки request.Settings.Federation.PrivateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()); request.Settings.Federation.PublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); } await _settingsService.UpdateSettingsAsync(request.Settings, cancellationToken); - + // Уведомляем другие модули (в т.ч. Федерацию) через Доменное Событие await _mediator.Publish(new Knot.Modules.Admin.Domain.Events.SystemSettingsUpdatedDomainEvent(request.Settings), cancellationToken); diff --git a/backend/src/Modules/Admin/Application/Admin/DTOs/CleanDryRunResult.cs b/backend/src/Modules/Admin/Application/Admin/DTOs/CleanDryRunResult.cs new file mode 100644 index 0000000..e714abd --- /dev/null +++ b/backend/src/Modules/Admin/Application/Admin/DTOs/CleanDryRunResult.cs @@ -0,0 +1,9 @@ +namespace Knot.Modules.Admin.Application.Admin.DTOs; + +public record CleanDryRunResult( + int OrphanedMessagesCount, + int OrphanedMediaCount, + long OrphanedMediaBytes, + int ExpiredStoriesCount, + long ExpiredStoriesSize +); diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs index 4c24fe5..e0cc42c 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs @@ -1,136 +1,77 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Knot.Contracts.Auth.Application.Abstractions; -using Knot.Contracts.Auth.Application.Auth.DTOs; -using Knot.Contracts.Messaging.Domain; +using Knot.Contracts.Admin.Abstractions; +using Knot.Contracts.Auth.Infrastructure.Persistence; +using Knot.Contracts.Conversations.Domain; +using Knot.Contracts.Conversations.Infrastructure.Persistence; +using Knot.Contracts.Messaging.Application.Abstractions; +using Knot.Contracts.Stories.Infrastructure.Persistence; using Knot.Modules.Admin.Application.Admin.DTOs; -using Knot.Modules.Auth.Infrastructure.Persistence; -using Knot.Modules.Auth.Application.Users; -using Knot.Modules.Conversations.Infrastructure.Persistence; -using Knot.Modules.Stories.Domain; -using Knot.Shared.Kernel; -using Knot.Shared.Kernel.Storage; using MediatR; using Microsoft.EntityFrameworkCore; -using MongoDB.Bson; -using MongoDB.Driver; namespace Knot.Modules.Admin.Application.Admin.Queries; -public record CleanDryRunQuery() : IQuery; +public record CleanDryRunQuery() : IQuery; -internal sealed class CleanDryRunQueryHandler : IQueryHandler +internal sealed class CleanDryRunQueryHandler : IQueryHandler { - private readonly ChatsDbContext _chatsDbContext; - private readonly IMongoCollection _messages; - private readonly IMongoCollection _stories; - private readonly IFileStorageService _fileStorage; - private readonly AuthDbContext _identityDb; + private readonly Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService _messageService; + private readonly IAuthDbContext _authDbContext; + private readonly IChatsDbContext _chatsDbContext; + private readonly IStoryCollection _storyCollection; - public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb, IFileStorageService fileStorage, AuthDbContext identityDb) + public CleanDryRunQueryHandler( + Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService messageService, + IAuthDbContext authDbContext, + IChatsDbContext chatsDbContext, + IStoryCollection storyCollection) { + _messageService = messageService; + _authDbContext = authDbContext; _chatsDbContext = chatsDbContext; - _messages = mongoDb.GetCollection("messages"); - _stories = mongoDb.GetCollection("stories"); - _fileStorage = fileStorage; - _identityDb = identityDb; + _storyCollection = storyCollection; } - public async Task> Handle(CleanDryRunQuery request, CancellationToken ct) + public async Task> Handle(CleanDryRunQuery request, CancellationToken ct) { try { - // 1. Получаем ID активных чатов (SQL) - var activeChats = await _chatsDbContext.Chats - .AsNoTracking() - .Select(c => new { c.Id, c.Avatar }) - .ToListAsync(ct); + var activeChatIds = _chatsDbContext.Chats.Select(c => c.Id).ToHashSet(); - var activeChatIds = activeChats.Select(c => c.Id).ToHashSet(); + var orphanedMessages = await _messageService.GetOrphanedMessagesAsync(activeChatIds, ct); + var orphanedMediaCount = orphanedMessages.Count(m => m.MediaUrl != null); + var orphanedMessageCount = orphanedMessages.Count; - // 2. Считаем сообщения подлежащие удалению (MongoDB) - var orphanedFilter = Builders.Filter.Or( - Builders.Filter.BitsAnySet(m => m.State, (long)MessageState.IsDeleted), - Builders.Filter.Nin(m => m.ChatId, activeChatIds) - ); - var orphanedMessagesCount = await _messages.CountDocumentsAsync(orphanedFilter, cancellationToken: ct); - - // 3. Собираем ID всех используемых файлов - var validFileIds = new HashSet(); - - // Аватары чатов и пользователей - foreach (var chat in activeChats) AddFileIdIfValid(chat.Avatar, validFileIds); - - var userAvatars = await _identityDb.Users.AsNoTracking() - .Where(u => !string.IsNullOrEmpty(u.Avatar)) - .Select(u => u.Avatar).ToListAsync(ct); - foreach (var avatar in userAvatars) AddFileIdIfValid(avatar, validFileIds); - - // Медиа из актуальных сторис - var storiesMedia = await _stories.Find(s => s.CreatedAt > DateTime.UtcNow.AddHours(-24)) - .Project(s => s.MediaUrl).ToListAsync(ct); - foreach (var url in storiesMedia) AddFileIdIfValid(url, validFileIds); - - // Медиа из активных сообщений (Проекция для скорости) - var activeFilter = Builders.Filter.And( - Builders.Filter.BitsAllClear(m => m.State, (long)MessageState.IsDeleted), - Builders.Filter.In(m => m.ChatId, activeChatIds) - ); - - var projection = Builders.Projection.Include("Media"); - - using (var cursor = await _messages.Find(activeFilter).Project(projection).ToCursorAsync(ct)) + var validIds = new HashSet(); + foreach (var msg in orphanedMessages.Where(m => m.MediaUrl != null)) { - while (await cursor.MoveNextAsync(ct)) + var parts = msg.MediaUrl.Split('/'); + var fileId = parts.LastOrDefault(); + if (!string.IsNullOrEmpty(fileId)) { - foreach (var doc in cursor.Current) - { - if (doc.Contains("Media") && doc["Media"].IsBsonArray) - { - foreach (var media in doc["Media"].AsBsonArray) - { - if (media.IsBsonDocument && media.AsBsonDocument.Contains("Url")) - AddFileIdIfValid(media.AsBsonDocument["Url"].AsString, validFileIds); - } - } - } + validIds.Add(fileId); } } - // 4. Анализ физического хранилища - var allStoredFiles = await _fileStorage.ListFilesAsync(); - long orphanedBytes = allStoredFiles - .Where(file => !validFileIds.Contains(file.FileId)) - .Sum(file => file.Size); + var stories = await _storyCollection.GetAllAsync(ct); + var expiredStoriesCount = stories.Count(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow); + var expiredStoriesSize = stories.Where(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow).Sum(s => s.MediaUrl?.Length ?? 0); - return Result.Success(new CleanupDryRunResultDto - { - OrphanedMessagesCount = (int)orphanedMessagesCount, - OrphanedMediaBytes = orphanedBytes - }); + return Result.Success(new CleanDryRunResult( + orphanedMessageCount, + orphanedMediaCount, + 0, + expiredStoriesCount, + expiredStoriesSize + )); } catch (Exception ex) { - return Result.Failure(new Error("Cleanup.Error", $"Ошибка при анализе данных: {ex.Message}")); - } - } - - private void AddFileIdIfValid(string? url, HashSet validIds) - { - if (string.IsNullOrEmpty(url)) return; - - // Предполагаем формат /api/files/{id} - if (url.Contains("/api/files/")) - { - var parts = url.Split('/'); - var fileId = parts.LastOrDefault(); - if (!string.IsNullOrEmpty(fileId)) - { - validIds.Add(fileId); - } + return Result.Failure(new Error("Admin.CleanDryRun.Error", $"Error during dry run: {ex.Message}")); } } } diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs index c5b12c0..009ec59 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs @@ -1,9 +1,9 @@ -using Knot.Modules.Admin.Application.Admin.DTOs; using System.Threading; using System.Threading.Tasks; +using Knot.Contracts.Admin.Abstractions; +using Knot.Contracts.Settings.Abstractions; +using Knot.Modules.Admin.Application.Admin.DTOs; using MediatR; -using Knot.Shared.Kernel; -using Knot.Shared.Kernel.Services; namespace Knot.Modules.Admin.Application.Admin.Queries; diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/GetSettings/GetSettingsQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/GetSettings/GetSettingsQuery.cs index 7bbfac4..6b6f545 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/GetSettings/GetSettingsQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/GetSettings/GetSettingsQuery.cs @@ -1,11 +1,11 @@ -using Knot.Modules.Admin.Application.Admin.DTOs; using System; using System.Threading; using System.Threading.Tasks; -using MediatR; -using Knot.Shared.Kernel; +using Knot.Contracts.Admin.Abstractions; using Knot.Contracts.Settings.Application.Abstractions; using Knot.Contracts.Settings.Application.DTOs; +using Knot.Modules.Admin.Application.Admin.DTOs; +using MediatR; namespace Knot.Modules.Admin.Application.Admin.Queries; diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs index eb82f1b..bc2ede1 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs @@ -1,91 +1,64 @@ -using Knot.Contracts.Auth.Domain; -using Knot.Contracts.Auth.Application.Auth.DTOs; -using Knot.Modules.Admin.Application.Admin.DTOs; -using Knot.Modules.Messaging.Domain; -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Knot.Contracts.Admin.Abstractions; +using Knot.Contracts.Auth.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Messaging.Application.Abstractions; +using Knot.Modules.Admin.Application.Admin.DTOs; using MediatR; -using Knot.Shared.Kernel; -using Knot.Modules.Conversations.Domain; -using MongoDB.Driver; namespace Knot.Modules.Admin.Application.Admin.Queries; -public record GetUserDetailsQuery(Guid UserId) : IQuery; +public record GetUserDetailsQuery(Guid UserId) : IQuery; -internal sealed class GetUserDetailsQueryHandler : IQueryHandler +internal sealed class GetUserDetailsQueryHandler : IQueryHandler { private readonly IUserRepository _userRepository; - private readonly IMongoCollection _messages; + private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService; + private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService; - public GetUserDetailsQueryHandler(IUserRepository userRepository, IMongoDatabase mongoDatabase) + public GetUserDetailsQueryHandler( + IUserRepository userRepository, + Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService, + Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService) { _userRepository = userRepository; - _messages = mongoDatabase.GetCollection("Messages"); + _statsService = statsService; + _statusService = statusService; } - public async Task> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetUserDetailsQuery request, CancellationToken ct) { - var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); - if (targetUser == null) + try { - return Result.Failure(AuthErrors.UserNotFound); - } + var user = await _userRepository.GetByIdAsync(request.UserId, ct); + if (user == null) + return Result.Failure(new Error("Admin.GetUserDetails.NotFound", "User not found")); - List userMessages; - try + var statsMap = await _statsService.GetStatsForUsersAsync(new List { request.UserId }, ct); + var stats = statsMap.GetValueOrDefault(request.UserId, new Knot.Contracts.Messaging.Application.Abstractions.UserStats(0, 0L)); + var isOnline = _statusService.IsUserOnline(request.UserId.ToString()); + + return Result.Success(new AdminUserDto( + user.Id, + user.Username ?? "unknown", + user.DisplayName ?? "User", + user.Email, + user.Avatar, + user.CreatedAt, + isOnline, + isOnline ? DateTime.UtcNow : (user.CreatedAt), + user.IsBanned, + stats.MessageCount, + stats.StorageSize + )); + } + catch (Exception ex) { - var filter = Builders.Filter.Eq(m => m.SenderId, request.UserId); - userMessages = await _messages.Find(filter).ToListAsync(cancellationToken); + return Result.Failure(new Error("Admin.GetUserDetails.Error", $"Error getting user details: {ex.Message}")); } - catch (Exception) - { - userMessages = new List(); - } - - var messagesCount = userMessages.Count; - - var mediaMessages = userMessages.OfType().ToList(); - var allUserMedia = mediaMessages - .Where(m => m.Media != null) - .SelectMany(m => m.Media) - .ToList(); - - var mediaCount = allUserMedia.Count(m => m.Type == "image" || m.Type == "video"); - var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio"); - var storageUsed = allUserMedia.Sum(m => m.Size ?? 0); - - var userContents = userMessages.OfType() - .Select(m => m.Content) - .Concat(mediaMessages.Where(m => m.Caption != null).Select(m => m.Caption)) - .Where(c => c != null) - .ToList(); - - var linksCount = userContents.Count(c => c != null && c.Contains("http", StringComparison.OrdinalIgnoreCase)); - - var isOnline = Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()); - - var result = new AdminUserDetailsDto( - targetUser.Id, - targetUser.Username ?? "unknown", - targetUser.DisplayName ?? "User", - targetUser.Bio, - targetUser.Avatar, - targetUser.CreatedAt, - isOnline, - isOnline ? DateTime.UtcNow : (targetUser.CreatedAt), - targetUser.IsBanned, - new AdminUserStatsDto( - messagesCount, - mediaCount, - filesCount, - linksCount, - storageUsed - ) - ); - - return Result.Success(result); } } diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/SearchUsers/SearchUsersQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/SearchUsers/SearchUsersQuery.cs index ea80d91..dee4aa8 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/SearchUsers/SearchUsersQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/SearchUsers/SearchUsersQuery.cs @@ -1,14 +1,14 @@ -using Knot.Contracts.Auth.Domain; -using Knot.Modules.Admin.Application.Admin.DTOs; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Knot.Contracts.Admin.Abstractions; +using Knot.Contracts.Auth.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Messaging.Application.Abstractions; +using Knot.Modules.Admin.Application.Admin.DTOs; using MediatR; -using Knot.Shared.Kernel; -using Knot.Modules.Messaging.Application.Abstractions; -using Knot.Modules.Conversations.Application.Abstractions; namespace Knot.Modules.Admin.Application.Admin.Queries; @@ -17,13 +17,13 @@ public record SearchUsersQuery(string Query) : IQuery>; internal sealed class SearchUsersQueryHandler : IQueryHandler> { private readonly IUserRepository _userRepository; - private readonly IUserStatsService _statsService; - private readonly IUserStatusService _statusService; + private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService; + private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService; public SearchUsersQueryHandler( - IUserRepository userRepository, - IUserStatsService statsService, - IUserStatusService statusService) + IUserRepository userRepository, + Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService, + Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService) { _userRepository = userRepository; _statsService = statsService; @@ -34,20 +34,19 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler()); var userIds = users.Select(u => u.Id).ToList(); - // 2. NoSQL var statsMap = await _statsService.GetStatsForUsersAsync(userIds, ct); - // 3. DTO - var result = users.Select(u => { - var stats = statsMap.GetValueOrDefault(u.Id, new UserStats(0, 0L)); + var result = users.Select(u => + { + var stats = statsMap.GetValueOrDefault(u.Id, new Knot.Contracts.Messaging.Application.Abstractions.UserStats(0, 0L)); var isOnline = _statusService.IsUserOnline(u.Id.ToString()); - + + return new AdminUserDto( u.Id, u.Username ?? "unknown", @@ -67,7 +66,7 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler>(new Error("Admin.SearchUsers.Error", $" : {ex.Message}")); + return Result.Failure>(new Error("Admin.SearchUsers.Error", $"Error searching users: {ex.Message}")); } } } diff --git a/backend/src/Modules/Admin/Domain/Events/SystemSettingsUpdatedDomainEvent.cs b/backend/src/Modules/Admin/Domain/Events/SystemSettingsUpdatedDomainEvent.cs index 91a8f74..5c42dfa 100644 --- a/backend/src/Modules/Admin/Domain/Events/SystemSettingsUpdatedDomainEvent.cs +++ b/backend/src/Modules/Admin/Domain/Events/SystemSettingsUpdatedDomainEvent.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Knot.Shared.Kernel; +using Knot.Contracts.Admin.Abstractions; using Knot.Contracts.Settings.Application.Abstractions; using Knot.Contracts.Settings.Application.DTOs; @@ -10,3 +10,4 @@ namespace Knot.Modules.Admin.Domain.Events; /// public sealed record SystemSettingsUpdatedDomainEvent(SystemSettingsDto Settings) : IDomainEvent; + diff --git a/backend/src/Modules/Admin/Knot.Modules.Admin.csproj b/backend/src/Modules/Admin/Knot.Modules.Admin.csproj index 534ad08..97466f0 100644 --- a/backend/src/Modules/Admin/Knot.Modules.Admin.csproj +++ b/backend/src/Modules/Admin/Knot.Modules.Admin.csproj @@ -4,9 +4,9 @@ enable enable + - - + @@ -14,14 +14,12 @@ - - - - - - + + + + \ No newline at end of file diff --git a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs index 337f68a..38a1d3d 100644 --- a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs +++ b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs @@ -1,22 +1,19 @@ using Carter; -using Knot.Contracts.Auth.Application.Auth.DTOs; -using Knot.Shared.Kernel; using Knot.Contracts.Settings.Application.Abstractions; using Knot.Contracts.Settings.Application.DTOs; -using Knot.Modules.Auth.Application.Users.Register; -using MediatR; -using Knot.Modules.Admin.Application.Admin.Queries; using Knot.Modules.Admin.Application.Admin.Commands; +using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy; +using Knot.Modules.Admin.Application.Admin.Queries; +using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Mvc; -using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy; -using Knot.Modules.Conversations.Application.Users.Commands.DeleteUser; +using Microsoft.AspNetCore.Routing; namespace Knot.Host.Presentation.Endpoints; public record KlipyTestDto(string ApiKey, string AppName); +public record ResetPasswordRequest(string NewPassword); public sealed class AdminEndpoints : ICarterModule { @@ -47,20 +44,11 @@ public sealed class AdminEndpoints : ICarterModule var result = await sender.Send(new GetDashboardStatsQuery(), ct); return Results.Ok(result.Value); }); - - group.MapPost("users", async ([FromBody] RegisterUserCommand command, ISender sender, CancellationToken ct) => - { - var result = await sender.Send(command, ct); - if (result.IsFailure) return Results.BadRequest(new { error = result.Error.Description }); - - var userDetails = await sender.Send(new GetUserDetailsQuery(result.Value.UserId), ct); - return Results.Ok(userDetails.Value); - }); - group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordDto dto, ISender sender, CancellationToken ct) => + group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordRequest dto, ISender sender, CancellationToken ct) => { var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct); - if (result.IsFailure) + if (result.IsFailure) { if (result.Error.Code == "User.NotFound") return Results.NotFound(new { error = "User not found" }); return Results.BadRequest(new { error = result.Error.Description }); @@ -80,12 +68,6 @@ public sealed class AdminEndpoints : ICarterModule return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description }); }); - group.MapDelete("users/{userId:guid}", async (Guid userId, ISender sender, CancellationToken ct) => - { - var result = await sender.Send(new DeleteUserCommand(userId), ct); - return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description }); - }); - group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) => { var result = await sender.Send(new SearchUsersQuery(query), ct); @@ -117,7 +99,8 @@ public sealed class AdminEndpoints : ICarterModule { // Получаем все системные часовые пояса и формируем удобный для фронтенда формат var zones = TimeZoneInfo.GetSystemTimeZones() - .Select(tz => new { + .Select(tz => new + { id = tz.Id, displayName = tz.DisplayName, standardName = tz.StandardName, diff --git a/backend/src/Modules/Auth/DependencyInjection.cs b/backend/src/Modules/Auth/DependencyInjection.cs index 6ed18e5..b4ecbeb 100644 --- a/backend/src/Modules/Auth/DependencyInjection.cs +++ b/backend/src/Modules/Auth/DependencyInjection.cs @@ -1,5 +1,6 @@ using Knot.Contracts.Auth.Application.Abstractions; using Knot.Contracts.Auth.Domain; +using Knot.Contracts.Auth.Infrastructure.Persistence; using Knot.Modules.Auth.Infrastructure.Authentication; using Knot.Modules.Auth.Infrastructure.Persistence; using Knot.Shared.Kernel; @@ -11,27 +12,22 @@ namespace Knot.Modules.Auth; public static class DependencyInjection { - /// - /// Регистрация сервисов модуля Identity. - /// public static IServiceCollection AddAuthModule( this IServiceCollection services, IConfiguration configuration) { - // Настройка базы данных string connectionString = configuration.GetConnectionString("DefaultConnection")!; services.AddDbContext(options => options.UseNpgsql(connectionString)); - // Регистрация Unit of Work и Репозиториев services.AddScoped(sp => sp.GetRequiredService()); - services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped(); services.AddScoped(); - // Регистрация MediatR для этого модуля services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); diff --git a/backend/src/Modules/Auth/Infrastructure/Persistence/AuthDbContext.cs b/backend/src/Modules/Auth/Infrastructure/Persistence/AuthDbContext.cs index 439186a..40dbaf9 100644 --- a/backend/src/Modules/Auth/Infrastructure/Persistence/AuthDbContext.cs +++ b/backend/src/Modules/Auth/Infrastructure/Persistence/AuthDbContext.cs @@ -1,5 +1,6 @@ using Knot.Contracts.Auth.Application.Abstractions; using Knot.Contracts.Auth.Domain; +using Knot.Contracts.Auth.Infrastructure.Persistence; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Security; using MediatR; @@ -9,7 +10,7 @@ using DomainUser = Knot.Modules.Auth.Domain.User; namespace Knot.Modules.Auth.Infrastructure.Persistence; -public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, IAuthDbContext +public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, Knot.Contracts.Auth.Application.Abstractions.IAuthDbContext, Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext { private readonly IMediator _mediator; private readonly IEncryptionService _encryptionService; @@ -31,6 +32,21 @@ public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, IAuthDbContext Set().Add(domainUser); } + IQueryable Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext.Users => + Set().Select(u => new UserContract + { + Id = u.Id, + Username = u.Username, + DisplayName = u.DisplayName, + Email = u.Email, + Avatar = u.Avatar, + PhoneNumber = u.PhoneNumber, + CreatedAt = u.CreatedAt, + IsBanned = u.IsBanned, + Bio = u.Bio, + PasswordHash = u.PasswordHash + }); + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); diff --git a/backend/src/Modules/Conversations/DependencyInjection.cs b/backend/src/Modules/Conversations/DependencyInjection.cs index 9753682..bcd56bc 100644 --- a/backend/src/Modules/Conversations/DependencyInjection.cs +++ b/backend/src/Modules/Conversations/DependencyInjection.cs @@ -1,4 +1,5 @@ using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Infrastructure.Persistence; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Modules.Conversations.Application.Abstractions; @@ -29,6 +30,7 @@ public static class DependencyInjection var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017"; services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs index e1f4782..540d299 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Infrastructure.Persistence; using Knot.Modules.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Domain; using Knot.Shared.Kernel; @@ -11,13 +12,11 @@ using Knot.Shared.Kernel.Security; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; +using DomainChat = Knot.Modules.Conversations.Domain.Chat; namespace Knot.Modules.Conversations.Infrastructure.Persistence; -/// -/// Контекст базы данных для модуля чатов. -/// -public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork +public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext { private readonly IMediator _mediator; private readonly IEncryptionService _encryptionService; @@ -29,10 +28,13 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli _encryptionService = encryptionService; } - public DbSet Chats => Set(); + public DbSet Chats => Set(); public DbSet Folders => Set(); public DbSet UserChatSettings => Set(); + IQueryable Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext.Chats => + Set().Select(c => new Knot.Contracts.Conversations.Infrastructure.Persistence.Chat { Avatar = c.Avatar }); + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); @@ -43,20 +45,20 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli { modelBuilder.HasDefaultSchema("chats"); - modelBuilder.Entity(builder => - { - builder.ToTable("Chats"); - builder.HasKey(c => c.Id); - builder.Property(c => c.Type).HasConversion(); + modelBuilder.Entity(builder => + { + builder.ToTable("Chats"); + builder.HasKey(c => c.Id); + builder.Property(c => c.Type).HasConversion(); - builder.OwnsMany(c => c.Members, mb => - { - mb.ToTable("ChatMembers"); - mb.HasKey(m => m.Id); - mb.WithOwner().HasForeignKey(m => m.ChatId); - mb.HasIndex(m => new { m.ChatId, m.UserId }).IsUnique(); - }).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field); - }); + builder.OwnsMany(c => c.Members, mb => + { + mb.ToTable("ChatMembers"); + mb.HasKey(m => m.Id); + mb.WithOwner().HasForeignKey(m => m.ChatId); + mb.HasIndex(m => new { m.ChatId, m.UserId }).IsUnique(); + }).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field); + }); modelBuilder.Entity(builder => { diff --git a/backend/src/Modules/Klipy/Infrastructure/External/KlipyClient.cs b/backend/src/Modules/Klipy/Infrastructure/External/KlipyClient.cs index 74e6fac..fb7c711 100644 --- a/backend/src/Modules/Klipy/Infrastructure/External/KlipyClient.cs +++ b/backend/src/Modules/Klipy/Infrastructure/External/KlipyClient.cs @@ -7,7 +7,7 @@ using Knot.Modules.Klipy.Application.Abstractions; namespace Knot.Modules.Klipy.Infrastructure.External; -public sealed class KlipyClient : IKlipyClient +public sealed class KlipyClient : Knot.Modules.Klipy.Application.Abstractions.IKlipyClient { private readonly HttpClient _httpClient; @@ -20,10 +20,9 @@ public sealed class KlipyClient : IKlipyClient { try { - // According to docs: https://api.klipy.com/api/v1/{app_key}/gifs/trending var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.klipy.com/api/v1/{appName}/gifs/trending?page=1&per_page=1&customer_id=knot_admin_test"); request.Headers.Add("X-KLIPY-API-KEY", apiKey); - + using var response = await _httpClient.SendAsync(request, ct); return response.IsSuccessStatusCode; } @@ -35,7 +34,6 @@ public sealed class KlipyClient : IKlipyClient public async Task> SearchVideosAsync(string apiKey, string appName, string query, int limit, CancellationToken ct = default) { - // To be implemented as needed return new List(); } } diff --git a/backend/src/Modules/Messaging/DependencyInjection.cs b/backend/src/Modules/Messaging/DependencyInjection.cs index d03aa2c..0ed12df 100644 --- a/backend/src/Modules/Messaging/DependencyInjection.cs +++ b/backend/src/Modules/Messaging/DependencyInjection.cs @@ -7,6 +7,8 @@ using Knot.Shared.Kernel; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; +using ContractUserStatsService = Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService; +using ModuleUserStatsService = Knot.Modules.Messaging.Application.Abstractions.IUserStatsService; namespace Knot.Modules.Messaging; @@ -27,7 +29,8 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(sp => (ContractUserStatsService)sp.GetRequiredService()); services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); diff --git a/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/UserStatsService.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/UserStatsService.cs index 332dc6e..2e7e836 100644 --- a/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/UserStatsService.cs +++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/UserStatsService.cs @@ -4,15 +4,17 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Messaging.Abstractions; +using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Modules.Messaging.Application.Abstractions; using MongoDB.Bson; using MongoDB.Driver; +using ContractUserStatsService = Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService; using ModuleUserStatsService = Knot.Modules.Messaging.Application.Abstractions.IUserStatsService; namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo; -public sealed class UserStatsService : ModuleUserStatsService +public sealed class UserStatsService : ModuleUserStatsService, ContractUserStatsService { private readonly IMongoCollection _messages; @@ -23,11 +25,10 @@ public sealed class UserStatsService : ModuleUserStatsService public async Task> GetStatsForUsersAsync(IEnumerable userIds, CancellationToken ct = default) { - var userIdList = userIds.ToList(); - if (!userIdList.Any()) return new Dictionary(); + if (!userIds.Any()) return new Dictionary(); var stats = await _messages.Aggregate() - .Match(Builders.Filter.In(m => m.SenderId, userIdList)) + .Match(Builders.Filter.In(m => m.SenderId, userIds)) .Group(new BsonDocument { { "_id", "$SenderId" }, { "Count", new BsonDocument("$sum", 1) }, @@ -44,6 +45,28 @@ public sealed class UserStatsService : ModuleUserStatsService ); } + async Task> Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService.GetStatsForUsersAsync(List userIds, CancellationToken cancellationToken = default) + { + if (!userIds.Any()) return new Dictionary(); + + var stats = await _messages.Aggregate() + .Match(Builders.Filter.In(m => m.SenderId, userIds)) + .Group(new BsonDocument { + { "_id", "$SenderId" }, + { "Count", new BsonDocument("$sum", 1) }, + { "MediaSize", new BsonDocument("$sum", new BsonDocument("$sum", "$Media.Size")) } + }) + .ToListAsync(cancellationToken); + + return stats.ToDictionary( + doc => doc["_id"].AsGuid, + doc => new Knot.Contracts.Messaging.Application.Abstractions.UserStats( + doc["Count"].AsInt32, + doc.Contains("MediaSize") && !doc["MediaSize"].IsBsonNull ? (long)(doc["MediaSize"].IsInt64 ? doc["MediaSize"].AsInt64 : doc["MediaSize"].AsInt32) : 0L + ) + ); + } + public async Task GetTotalStorageSizeAsync(CancellationToken ct = default) { var result = await _messages.Aggregate() diff --git a/backend/src/Modules/Stories/DependencyInjection.cs b/backend/src/Modules/Stories/DependencyInjection.cs index 8a599f3..646c207 100644 --- a/backend/src/Modules/Stories/DependencyInjection.cs +++ b/backend/src/Modules/Stories/DependencyInjection.cs @@ -1,10 +1,11 @@ +using System.Net.Http; +using Knot.Contracts.Stories.Infrastructure.Persistence; +using Knot.Modules.Stories.Application.Abstractions; +using Knot.Modules.Stories.Infrastructure.Persistence.Mongo; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Knot.Modules.Stories.Application.Abstractions; -using Knot.Modules.Stories.Infrastructure.Persistence.Mongo; using MongoDB.Driver; -using System.Net.Http; namespace Knot.Modules.Stories; @@ -13,12 +14,12 @@ public static class DependencyInjection public static IServiceCollection AddStoriesModule(this IServiceCollection services, IConfiguration configuration) { services.AddScoped(); - + services.AddScoped(); + var connectionString = configuration.GetConnectionString("DefaultConnection"); services.AddDbContext(options => options.UseNpgsql(connectionString)); - // MongoDB configuration happens in StoriesMongoMapConfigurator (called in Program.cs when EncryptionService is ready, or we can just call it here) StoriesMongoMapConfigurator.Configure(); services.AddMediatR(config => diff --git a/backend/src/Modules/Stories/Infrastructure/Persistence/Mongo/StoryCollection.cs b/backend/src/Modules/Stories/Infrastructure/Persistence/Mongo/StoryCollection.cs new file mode 100644 index 0000000..4e28631 --- /dev/null +++ b/backend/src/Modules/Stories/Infrastructure/Persistence/Mongo/StoryCollection.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Knot.Contracts.Stories.Infrastructure.Persistence; +using MongoDB.Driver; +using DomainStory = Knot.Modules.Stories.Domain.Story; + +namespace Knot.Modules.Stories.Infrastructure.Persistence.Mongo; + +public class StoryCollection : Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection +{ + private readonly IMongoCollection _stories; + + public StoryCollection(IMongoDatabase database) + { + _stories = database.GetCollection("stories"); + } + + public async Task> GetAllAsync(CancellationToken cancellationToken) + { + var stories = await _stories.Find(_ => true).ToListAsync(cancellationToken); + return stories.Select(s => new Knot.Contracts.Stories.Infrastructure.Persistence.StoryContract + { + Id = s.Id, + UserId = s.UserId, + MediaUrl = s.MediaUrl, + MediaType = s.Type.ToString(), + CreatedAt = s.CreatedAt, + ExpiresAt = null + }).ToList(); + } +} \ No newline at end of file