Перепиливание под чистый DDD
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
|
||||
public record CleanRunCommand(IFileStorageService FileStorage, AuthDbContext IdentityDb) : ICommand<MessageResponse>;
|
||||
|
||||
internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand, MessageResponse>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
}
|
||||
|
||||
public async Task<Result<MessageResponse>> Handle(CleanRunCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var orphanMessages = allMessages
|
||||
.Where(m => !activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var keptMessages = allMessages
|
||||
.Where(m => activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
.Where(u => !string.IsNullOrEmpty(u));
|
||||
|
||||
var activeChatUrls = allChats
|
||||
.Where(c => !string.IsNullOrEmpty(c.Avatar))
|
||||
.Select(c => c.Avatar!);
|
||||
|
||||
var activeUserUrls = allUsers
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar!);
|
||||
|
||||
foreach (var u in activeMessageUrls)
|
||||
{
|
||||
validUrls.Add(u!);
|
||||
}
|
||||
foreach (var u in activeChatUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
foreach (var u in activeUserUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
|
||||
var validFileIds = validUrls
|
||||
.Where(u => u.Contains("/api/files/"))
|
||||
.Select(u => u.Split('/').Last())
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var file in allMinioFiles)
|
||||
{
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
{
|
||||
await request.FileStorage.DeleteFileAsync(file.FileId);
|
||||
}
|
||||
}
|
||||
|
||||
if (orphanMessages.Any())
|
||||
{
|
||||
var orphanIds = orphanMessages.Select(m => m.Id).ToList();
|
||||
var filter = Builders<Message>.Filter.In(m => m.Id, orphanIds);
|
||||
await _messages.DeleteManyAsync(filter, cancellationToken);
|
||||
}
|
||||
|
||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
|
||||
public record ResetUserPasswordCommand(Guid UserId, string NewPassword) : ICommand<SuccessResponse>;
|
||||
|
||||
internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUserPasswordCommand, SuccessResponse>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IAuthUnitOfWork _identityUnitOfWork;
|
||||
|
||||
public ResetUserPasswordCommandHandler(IUserRepository userRepository, IAuthUnitOfWork identityUnitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_identityUnitOfWork = identityUnitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<SuccessResponse>> Handle(ResetUserPasswordCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<SuccessResponse>(AuthErrors.UserNotFound);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
return Result.Failure<SuccessResponse>(DomainErrors.InvalidPassword);
|
||||
|
||||
var hash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
|
||||
user.ChangePassword(hash);
|
||||
|
||||
await _identityUnitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(new SuccessResponse(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
|
||||
public record UpdateSettingsCommand(SystemSettingsDto Settings) : ICommand<SystemSettingsDto>;
|
||||
|
||||
internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSettingsCommand, SystemSettingsDto>
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public UpdateSettingsCommandHandler(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task<Result<SystemSettingsDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await _settingsService.UpdateSettingsAsync(request.Settings, cancellationToken);
|
||||
return Result.Success(request.Settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public record AdminUserDetailsDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
DateTime CreatedAt,
|
||||
bool IsOnline,
|
||||
DateTime LastOnlineAt,
|
||||
AdminUserStatsDto Stats
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public record AdminUserDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Email,
|
||||
string? Avatar,
|
||||
DateTime CreatedAt,
|
||||
bool IsOnline,
|
||||
DateTime LastOnlineAt
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public record AdminUserStatsDto(
|
||||
int MessagesCount,
|
||||
int MediaCount,
|
||||
int FilesCount,
|
||||
int LinksCount,
|
||||
long StorageUsedBytes
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public class CleanupDryRunResultDto
|
||||
{
|
||||
public int OrphanedMessagesCount { get; set; }
|
||||
public long OrphanedMediaBytes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record CleanDryRunQuery(IFileStorageService FileStorage, AuthDbContext IdentityDb) : IQuery<CleanupDryRunResultDto>;
|
||||
|
||||
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanupDryRunResultDto>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
}
|
||||
|
||||
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var orphanedMessages = allMessages
|
||||
.Where(m => !activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var keptMessages = allMessages
|
||||
.Where(m => activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var orphanedMessagesCount = orphanedMessages.Count;
|
||||
|
||||
var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
.Where(u => !string.IsNullOrEmpty(u));
|
||||
|
||||
var activeChatUrls = allChats
|
||||
.Where(c => !string.IsNullOrEmpty(c.Avatar))
|
||||
.Select(c => c.Avatar!);
|
||||
|
||||
var activeUserUrls = allUsers
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar!);
|
||||
|
||||
foreach (var u in activeMessageUrls)
|
||||
{
|
||||
validUrls.Add(u!);
|
||||
}
|
||||
foreach (var u in activeChatUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
foreach (var u in activeUserUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
|
||||
var validFileIds = validUrls
|
||||
.Where(u => u.Contains("/api/files/"))
|
||||
.Select(u => u.Split('/').Last())
|
||||
.ToHashSet();
|
||||
|
||||
long safeBytes = 0;
|
||||
foreach (var file in allMinioFiles)
|
||||
{
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
{
|
||||
safeBytes += file.Size;
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success(new CleanupDryRunResultDto
|
||||
{
|
||||
OrphanedMessagesCount = orphanedMessagesCount,
|
||||
OrphanedMediaBytes = safeBytes
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Services;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record GetDashboardStatsQuery() : IQuery<DashboardStatsDto>;
|
||||
|
||||
internal sealed class GetDashboardStatsQueryHandler : IQueryHandler<GetDashboardStatsQuery, DashboardStatsDto>
|
||||
{
|
||||
private readonly IStatisticsService _statisticsService;
|
||||
|
||||
public GetDashboardStatsQueryHandler(IStatisticsService statisticsService)
|
||||
{
|
||||
_statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
public async Task<Result<DashboardStatsDto>> Handle(GetDashboardStatsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var stats = await _statisticsService.GetDashboardStatsAsync(cancellationToken);
|
||||
return Result.Success(stats);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record GetSettingsQuery() : IQuery<SystemSettingsDto>;
|
||||
|
||||
internal sealed class GetSettingsQueryHandler : IQueryHandler<GetSettingsQuery, SystemSettingsDto>
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public GetSettingsQueryHandler(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task<Result<SystemSettingsDto>> Handle(GetSettingsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetSettingsAsync(cancellationToken);
|
||||
return Result.Success(settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record GetUserDetailsQuery(Guid UserId) : IQuery<AdminUserDetailsDto>;
|
||||
|
||||
internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQuery, AdminUserDetailsDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public GetUserDetailsQueryHandler(IUserRepository userRepository, IMongoDatabase mongoDatabase)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_messages = mongoDatabase.GetCollection<Message>("Messages");
|
||||
}
|
||||
|
||||
public async Task<Result<AdminUserDetailsDto>> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (targetUser == null)
|
||||
{
|
||||
return Result.Failure<AdminUserDetailsDto>(AuthErrors.UserNotFound);
|
||||
}
|
||||
|
||||
var filter = Builders<Message>.Filter.Eq(m => m.SenderId, request.UserId);
|
||||
var userMessages = await _messages.Find(filter).ToListAsync(cancellationToken);
|
||||
|
||||
var messagesCount = userMessages.Count;
|
||||
|
||||
var allUserMedia = userMessages.OfType<MediaMessage>().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<TextMessage>().Select(m => m.Content)
|
||||
.Concat(userMessages.OfType<MediaMessage>().Where(m => m.Caption != null).Select(m => m.Caption))
|
||||
.ToList();
|
||||
|
||||
var linksCount = userContents.Count(c => !string.IsNullOrEmpty(c) && c.Contains("http"));
|
||||
|
||||
var result = new AdminUserDetailsDto(
|
||||
targetUser.Id,
|
||||
targetUser.Username,
|
||||
targetUser.DisplayName,
|
||||
targetUser.Bio,
|
||||
targetUser.Avatar,
|
||||
targetUser.CreatedAt,
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()),
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()) ? DateTime.UtcNow : targetUser.CreatedAt,
|
||||
new AdminUserStatsDto(
|
||||
messagesCount,
|
||||
mediaCount,
|
||||
filesCount,
|
||||
linksCount,
|
||||
storageUsed
|
||||
)
|
||||
);
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record SearchUsersQuery(string Query) : IQuery<List<AdminUserDto>>;
|
||||
|
||||
internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery, List<AdminUserDto>>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public SearchUsersQueryHandler(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<AdminUserDto>>> Handle(SearchUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var users = string.IsNullOrWhiteSpace(request.Query)
|
||||
? await _userRepository.SearchUsersAsync("", cancellationToken)
|
||||
: await _userRepository.SearchUsersAsync(request.Query, cancellationToken);
|
||||
|
||||
var result = users.Select(u => new AdminUserDto(
|
||||
u.Id,
|
||||
u.Username,
|
||||
u.DisplayName,
|
||||
u.Email,
|
||||
u.Avatar,
|
||||
u.CreatedAt,
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()),
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()) ? DateTime.UtcNow : u.CreatedAt
|
||||
)).ToList();
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
7
backend/src/Modules/Admin/DependencyInjection.cs
Normal file
7
backend/src/Modules/Admin/DependencyInjection.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Modules.Admin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
public static class DependencyInjection {
|
||||
public static IServiceCollection AddAdminModule(this IServiceCollection services) {
|
||||
return services;
|
||||
}
|
||||
}
|
||||
22
backend/src/Modules/Admin/Knot.Modules.Admin.csproj
Normal file
22
backend/src/Modules/Admin/Knot.Modules.Admin.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
|
||||
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
|
||||
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>Knot.Modules.Admin.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,86 @@
|
||||
using Carter;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Auth.Application.Users.Register;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using MediatR;
|
||||
using Host.Application.Admin.Queries;
|
||||
using Host.Application.Admin.Commands;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
|
||||
public sealed class AdminEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("api/admin"); // Middleware handles auth
|
||||
|
||||
group.MapGet("settings", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetSettingsQuery(), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPut("settings", async ([FromBody] SystemSettingsDto settings, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new UpdateSettingsCommand(settings), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapGet("dashboard", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
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.User.Id), ct);
|
||||
return Results.Ok(userDetails.Value);
|
||||
});
|
||||
|
||||
group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordDto dto, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct);
|
||||
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 });
|
||||
}
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) =>
|
||||
{
|
||||
var result = await sender.Send(new SearchUsersQuery(query), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapGet("users/{id:guid}", async (Guid id, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetUserDetailsQuery(id), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found");
|
||||
});
|
||||
|
||||
group.MapGet("clean/dry-run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new CleanDryRunQuery(fileStorage, identityDb), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("clean/run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new CleanRunCommand(fileStorage, identityDb), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Abstractions;
|
||||
namespace Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
public interface IIdentityDbContext
|
||||
public interface IAuthDbContext
|
||||
{
|
||||
DbSet<User> Users { get; }
|
||||
DbSet<Friendship> Friendships { get; }
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Abstractions;
|
||||
namespace Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work специфичный для модуля Identity.
|
||||
/// </summary>
|
||||
public interface IIdentityUnitOfWork : IUnitOfWork
|
||||
public interface IAuthUnitOfWork : IUnitOfWork
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
public interface IJwtTokenProvider
|
||||
{
|
||||
string Generate(User user);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Auth;
|
||||
namespace Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
public record AuthResponseDto(
|
||||
string Token,
|
||||
@@ -18,3 +18,4 @@ public record AuthUserDto(
|
||||
bool IsOnline,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
public class ResetPasswordDto
|
||||
{
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Auth.Application.Users.Auth;
|
||||
|
||||
public record AuthResponseDto(
|
||||
string Token,
|
||||
AuthUserDto User
|
||||
);
|
||||
|
||||
public record AuthUserDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Email,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
DateTime? Birthday,
|
||||
bool IsOnline,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Application.Users.Auth;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.GetMe;
|
||||
using Knot.Modules.Auth.Application.Users.Auth;
|
||||
|
||||
namespace Knot.Modules.Auth.Application.Users.GetMe;
|
||||
|
||||
public sealed record GetMeQuery(Guid UserId) : IQuery<AuthResponseDto>;
|
||||
|
||||
@@ -21,7 +21,7 @@ internal sealed class GetMeQueryHandler : IQueryHandler<GetMeQuery, AuthResponse
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.UserNotFound);
|
||||
return Result.Failure<AuthResponseDto>(AuthErrors.UserNotFound);
|
||||
}
|
||||
|
||||
var response = new AuthResponseDto(
|
||||
@@ -42,3 +42,4 @@ internal sealed class GetMeQueryHandler : IQueryHandler<GetMeQuery, AuthResponse
|
||||
return Result.Success(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Application.Users.Auth;
|
||||
using Knot.Modules.Auth.Application.Users.Auth;
|
||||
using BCrypt.Net;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Login;
|
||||
namespace Knot.Modules.Auth.Application.Users.Login;
|
||||
|
||||
/// <summary>
|
||||
/// Команда для входа пользователя. Возвращает AuthResponseDto.
|
||||
@@ -28,7 +29,7 @@ public sealed class LoginUserCommandHandler : ICommandHandler<LoginUserCommand,
|
||||
|
||||
if (user is null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityInvalidCredentials);
|
||||
return Result.Failure<AuthResponseDto>(AuthErrors.IdentityInvalidCredentials);
|
||||
}
|
||||
|
||||
string token = _tokenProvider.Generate(user);
|
||||
@@ -49,3 +50,4 @@ public sealed class LoginUserCommandHandler : ICommandHandler<LoginUserCommand,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Identity.Application.Users.Auth;
|
||||
using Knot.Modules.Auth.Application.Users.Auth;
|
||||
using BCrypt.Net;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Register;
|
||||
namespace Knot.Modules.Auth.Application.Users.Register;
|
||||
|
||||
/// <summary>
|
||||
/// Команда для регистрации нового пользователя.
|
||||
@@ -23,13 +24,13 @@ public sealed record RegisterUserCommand(
|
||||
public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCommand, AuthResponseDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
private readonly IAuthUnitOfWork _unitOfWork;
|
||||
private readonly ISettingsService _settings;
|
||||
private readonly IJwtTokenProvider _tokenProvider;
|
||||
|
||||
public RegisterUserCommandHandler(
|
||||
IUserRepository userRepository,
|
||||
IIdentityUnitOfWork unitOfWork,
|
||||
IAuthUnitOfWork unitOfWork,
|
||||
ISettingsService settings,
|
||||
IJwtTokenProvider tokenProvider)
|
||||
{
|
||||
@@ -43,13 +44,13 @@ public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCom
|
||||
{
|
||||
if (!_settings.Current.EnableRegistration)
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityRegistrationDisabled);
|
||||
return Result.Failure<AuthResponseDto>(AuthErrors.IdentityRegistrationDisabled);
|
||||
}
|
||||
|
||||
// 1. Проверка уникальности username
|
||||
if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken))
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityUsernameNotUnique);
|
||||
return Result.Failure<AuthResponseDto>(AuthErrors.IdentityUsernameNotUnique);
|
||||
}
|
||||
|
||||
// 2. Хеширование пароля (здесь будет вызов сервиса, пока заглушка)
|
||||
@@ -85,3 +86,4 @@ public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCom
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Infrastructure.Authentication;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
using Knot.Modules.Auth.Infrastructure.Authentication;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity;
|
||||
namespace Knot.Modules.Auth;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Регистрация сервисов модуля Identity.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddIdentityModule(
|
||||
public static IServiceCollection AddAuthModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Настройка базы данных
|
||||
string connectionString = configuration.GetConnectionString("DefaultConnection")!;
|
||||
|
||||
services.AddDbContext<IdentityDbContext>(options =>
|
||||
services.AddDbContext<AuthDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Регистрация Unit of Work и Репозиториев
|
||||
services.AddScoped<IIdentityUnitOfWork>(sp => sp.GetRequiredService<IdentityDbContext>());
|
||||
services.AddScoped<IIdentityDbContext>(sp => sp.GetRequiredService<IdentityDbContext>());
|
||||
services.AddScoped<IAuthUnitOfWork>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||
services.AddScoped<IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IJwtTokenProvider, JwtTokenProvider>();
|
||||
services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Identity.Infrastructure.Services.UserDisplayNameProvider>();
|
||||
services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Auth.Infrastructure.Services.UserDisplayNameProvider>();
|
||||
|
||||
// Регистрация MediatR для этого модуля
|
||||
services.AddMediatR(config =>
|
||||
@@ -38,3 +38,4 @@ public static class DependencyInjection
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
namespace Knot.Modules.Auth.Domain;
|
||||
|
||||
public static class IdentityErrors
|
||||
public static class AuthErrors
|
||||
{
|
||||
public static readonly Error FriendsNotFound = new Error("Friends.NotFound", "Friendship not found");
|
||||
public static readonly Error FriendsSelf = new Error("Friends.Self", "Cannot add yourself");
|
||||
@@ -12,3 +12,4 @@ public static class IdentityErrors
|
||||
public static readonly Error IdentityRegistrationDisabled = new Error("Identity.RegistrationDisabled", "Registration is disabled by the administrator.");
|
||||
public static readonly Error IdentityUsernameNotUnique = new Error("Identity.UsernameNotUnique", "Это имя пользователя уже занято.");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
namespace Knot.Modules.Auth.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс репозитория для работы с пользователями.
|
||||
@@ -15,3 +15,4 @@ public interface IUserRepository
|
||||
void Add(User user);
|
||||
void Update(User user);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
namespace Knot.Modules.Auth.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Сущность пользователя в контексте идентификации (Identity).
|
||||
@@ -58,3 +58,4 @@ public sealed class User : AggregateRoot<Guid>
|
||||
PasswordHash = newPasswordHash;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Authentication;
|
||||
namespace Knot.Modules.Auth.Infrastructure.Authentication;
|
||||
|
||||
public sealed class JwtTokenProvider : IJwtTokenProvider
|
||||
{
|
||||
@@ -43,3 +43,4 @@ public sealed class JwtTokenProvider : IJwtTokenProvider
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
|
||||
namespace Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Контекст базы данных для модуля Identity.
|
||||
/// </summary>
|
||||
public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, Knot.Modules.Auth.Application.Abstractions.IAuthDbContext
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
|
||||
public AuthDbContext(DbContextOptions<AuthDbContext> options, IMediator mediator, IEncryptionService encryptionService)
|
||||
: base(options)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_encryptionService = encryptionService;
|
||||
}
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
|
||||
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema("identity");
|
||||
|
||||
|
||||
|
||||
modelBuilder.Entity<User>(builder =>
|
||||
{
|
||||
builder.ToTable("Users");
|
||||
builder.HasKey(u => u.Id);
|
||||
builder.Property(u => u.Username).IsRequired().HasMaxLength(50);
|
||||
builder.HasIndex(u => u.Username).IsUnique();
|
||||
builder.Property(u => u.PasswordHash).IsRequired();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var domainEvents = ChangeTracker
|
||||
.Entries<IAggregateRoot>()
|
||||
.SelectMany(x =>
|
||||
|
||||
{
|
||||
if (x.Entity is AggregateRoot<Guid> root)
|
||||
{
|
||||
var events = root.GetDomainEvents().ToList();
|
||||
root.ClearDomainEvents();
|
||||
return events;
|
||||
}
|
||||
return Enumerable.Empty<IDomainEvent>();
|
||||
})
|
||||
.ToList();
|
||||
|
||||
int result = await base.SaveChangesAsync(cancellationToken);
|
||||
|
||||
foreach (var domainEvent in domainEvents)
|
||||
{
|
||||
await _mediator.Publish(domainEvent, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
namespace Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация репозитория пользователей с использованием EF Core.
|
||||
/// </summary>
|
||||
public sealed class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly IdentityDbContext _context;
|
||||
private readonly AuthDbContext _context;
|
||||
|
||||
public UserRepository(IdentityDbContext context)
|
||||
public UserRepository(AuthDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
@@ -54,3 +54,4 @@ public sealed class UserRepository : IUserRepository
|
||||
_context.Users.Update(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Services;
|
||||
namespace Knot.Modules.Auth.Infrastructure.Services;
|
||||
|
||||
public sealed class UserDisplayNameProvider : IUserDisplayNameProvider
|
||||
{
|
||||
@@ -40,3 +40,4 @@ public sealed class UserDisplayNameProvider : IUserDisplayNameProvider
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\Profiles\Knot.Modules.Profiles.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||
@@ -1,19 +1,19 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
namespace Knot.Modules.Auth.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
[Migration("20260311180816_InitialIdentity")]
|
||||
partial class InitialIdentity
|
||||
[DbContext(typeof(AuthDbContext))]
|
||||
[Migration("20260322191927_InitialAuth")]
|
||||
partial class InitialAuth
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -26,20 +26,33 @@ namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b =>
|
||||
modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("HideStoryViews")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
namespace Knot.Modules.Auth.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialIdentity : Migration
|
||||
public partial class InitialAuth : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
@@ -22,8 +22,13 @@ namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Username = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Email = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true)
|
||||
DisplayName = table.Column<string>(type: "text", nullable: false),
|
||||
Email = table.Column<string>(type: "text", nullable: true),
|
||||
Bio = table.Column<string>(type: "text", nullable: true),
|
||||
Avatar = table.Column<string>(type: "text", nullable: true),
|
||||
Birthday = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
HideStoryViews = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -0,0 +1,73 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Auth.Migrations
|
||||
{
|
||||
[DbContext(typeof(AuthDbContext))]
|
||||
partial class AuthDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("identity")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("HideStoryViews")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Carter;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Users.Login;
|
||||
using Knot.Modules.Auth.Application.Users.Register;
|
||||
using Knot.Modules.Auth.Application.Users.GetMe;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Knot.Modules.Auth.Presentation.Endpoints;
|
||||
|
||||
public sealed class AuthEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("api/auth");
|
||||
|
||||
group.MapPost("register", async ([FromBody] RegisterUserCommand command, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(command, ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Code ?? result.Error.Description });
|
||||
});
|
||||
|
||||
group.MapPost("login", async ([FromBody] LoginUserCommand command, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(command, ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.Unauthorized();
|
||||
});
|
||||
|
||||
group.MapGet("me", async (ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetMeQuery(userContext.UserId), ct);
|
||||
return result.IsSuccess ? Results.Ok(new { User = result.Value.User }) : Results.NotFound();
|
||||
}).RequireAuthorization();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
|
||||
public sealed record CreatePersonalChatRequest(Guid UserId);
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
|
||||
public record TogglePinResponse(bool IsPinned);
|
||||
@@ -1,99 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChatsDbContext))]
|
||||
[Migration("20260319124845_InitialChats")]
|
||||
partial class InitialChats
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHighWaterMark : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "LastMessageSequenceId",
|
||||
schema: "chats",
|
||||
table: "Chats",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "LastDeliveredMessageId",
|
||||
schema: "chats",
|
||||
table: "ChatMembers",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "LastReadMessageId",
|
||||
schema: "chats",
|
||||
table: "ChatMembers",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "LastReadSequenceId",
|
||||
schema: "chats",
|
||||
table: "ChatMembers",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastMessageSequenceId",
|
||||
schema: "chats",
|
||||
table: "Chats");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDeliveredMessageId",
|
||||
schema: "chats",
|
||||
table: "ChatMembers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastReadMessageId",
|
||||
schema: "chats",
|
||||
table: "ChatMembers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastReadSequenceId",
|
||||
schema: "chats",
|
||||
table: "ChatMembers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Abstractions;
|
||||
namespace Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work специфичный для модуля Chats.
|
||||
@@ -8,3 +8,4 @@ namespace Knot.Modules.Chats.Application.Abstractions;
|
||||
public interface IChatsUnitOfWork : IUnitOfWork
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using MediatR;
|
||||
using System.Linq;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.Avatar;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.Avatar;
|
||||
|
||||
public record UploadGroupAvatarCommand(Guid ChatId, Guid UserId, string FileName, string ContentType, Stream FileStream) : ICommand<Guid>;
|
||||
|
||||
@@ -127,3 +127,4 @@ internal sealed class RemoveGroupAvatarCommandHandler : ICommandHandler<RemoveGr
|
||||
return Result.Success(request.ChatId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using MediatR;
|
||||
using System.Linq;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.Clear;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.Clear;
|
||||
|
||||
public record ClearChatCommand(Guid ChatId, Guid UserId) : ICommand<MessageResponse>;
|
||||
|
||||
@@ -32,3 +32,4 @@ internal sealed class ClearChatCommandHandler : ICommandHandler<ClearChatCommand
|
||||
return Result.Success(new MessageResponse("Cleared"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.Create;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.Create;
|
||||
|
||||
/// <summary>
|
||||
/// Команда для создания чата.
|
||||
@@ -41,3 +41,4 @@ public sealed class CreateChatCommandHandler : ICommandHandler<CreateChatCommand
|
||||
return Result.Success(chat.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -5,11 +7,11 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.DTOs;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.GetChatById;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.GetChatById;
|
||||
|
||||
public record GetChatByIdQuery(Guid UserId, Guid ChatId) : IQuery<ChatDto?>;
|
||||
|
||||
@@ -153,3 +155,5 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -5,12 +7,12 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.DTOs;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.GetChats;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.GetChats;
|
||||
|
||||
public record GetChatsQuery(Guid UserId) : IQuery<List<ChatDto>>;
|
||||
|
||||
@@ -147,3 +149,5 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.GetOrCreateFavorites;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites;
|
||||
|
||||
public sealed record GetOrCreateFavoritesCommand(Guid UserId) : ICommand<Guid>;
|
||||
|
||||
@@ -36,3 +36,4 @@ public sealed class GetOrCreateFavoritesCommandHandler : ICommandHandler<GetOrCr
|
||||
return Result.Success(chat.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using MediatR;
|
||||
using System.Linq;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.LeaveOrDelete;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.LeaveOrDelete;
|
||||
|
||||
public record LeaveOrDeleteChatCommand(Guid ChatId, Guid UserId) : ICommand<SuccessResponse>;
|
||||
|
||||
@@ -51,3 +51,4 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler<LeaveOrD
|
||||
return Result.Success(new SuccessResponse(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using MediatR;
|
||||
using System.Linq;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.Members;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.Members;
|
||||
|
||||
public record AddMembersCommand(Guid ChatId, Guid UserId, List<Guid> UserIdsToAdd) : ICommand<Guid>;
|
||||
|
||||
@@ -70,3 +70,4 @@ internal sealed class RemoveMemberCommandHandler : ICommandHandler<RemoveMemberC
|
||||
return Result.Success(request.ChatId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Chats.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using MediatR;
|
||||
using System.Linq;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.TogglePin;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.TogglePin;
|
||||
|
||||
public record TogglePinCommand(Guid ChatId, Guid UserId) : ICommand<TogglePinResponse>;
|
||||
|
||||
@@ -44,3 +44,4 @@ internal sealed class TogglePinCommandHandler : ICommandHandler<TogglePinCommand
|
||||
return Result.Success(new TogglePinResponse(member.IsPinned));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using MediatR;
|
||||
using System.Linq;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Chats.Update;
|
||||
namespace Knot.Modules.Conversations.Application.Chats.Update;
|
||||
|
||||
public record UpdateChatCommand(Guid ChatId, Guid UserId, string? Name, string? Description) : ICommand<Guid>;
|
||||
|
||||
@@ -46,3 +46,4 @@ internal sealed class UpdateChatCommandHandler : ICommandHandler<UpdateChatComma
|
||||
return Result.Success(chat.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public sealed record AddMembersRequest(List<Guid> UserIds);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record ChatDto(
|
||||
Guid Id,
|
||||
@@ -14,3 +14,4 @@ public record ChatDto(
|
||||
List<ChatMessageDto> Messages,
|
||||
int UnreadCount
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record ChatMemberDto(
|
||||
Guid Id,
|
||||
@@ -10,3 +10,4 @@ public record ChatMemberDto(
|
||||
bool IsPinned,
|
||||
ChatUserDto? User
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record ChatMessageDto(
|
||||
Guid Id,
|
||||
@@ -23,3 +23,4 @@ public record ChatMessageDto(
|
||||
List<ReactionDto> Reactions,
|
||||
List<ReadByDto> ReadBy
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record ChatUserDto(
|
||||
Guid Id,
|
||||
@@ -10,3 +10,4 @@ public record ChatUserDto(
|
||||
bool IsOnline,
|
||||
DateTime LastSeen
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public sealed record CreateChatRequest(string Name, ChatType Type, List<Guid> MemberIds);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public sealed record CreateGroupChatRequest(string Name, List<Guid> MemberIds);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
using System;
|
||||
|
||||
public record CreatePersonalChatRequest(Guid UserId);
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record MediaDto(
|
||||
Guid Id,
|
||||
@@ -9,3 +9,4 @@ public record MediaDto(
|
||||
string? Filename,
|
||||
long? Size
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record MessageDetailDto(
|
||||
Guid Id,
|
||||
@@ -41,3 +41,4 @@ public record MessageReactionDto(
|
||||
Guid UserId,
|
||||
MessageSenderDto? User
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record MessageSenderDto(
|
||||
Guid Id,
|
||||
@@ -8,3 +8,4 @@ public record MessageSenderDto(
|
||||
string DisplayName,
|
||||
string? Avatar
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record ReactionDto(
|
||||
Guid Id,
|
||||
@@ -8,3 +8,4 @@ public record ReactionDto(
|
||||
Guid UserId,
|
||||
MessageSenderDto User
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record ReadByDto(
|
||||
Guid UserId
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record SearchMessageDto(
|
||||
Guid Id,
|
||||
@@ -30,3 +30,4 @@ public record SimpleReactionDto(
|
||||
Guid UserId,
|
||||
string Emoji
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public sealed record SendMessageRequest(
|
||||
string? Content,
|
||||
@@ -12,3 +12,4 @@ public sealed record SendMessageRequest(
|
||||
Guid? ForwardedFromId = null);
|
||||
|
||||
public sealed record AttachmentDto(string Type, string Url, string? FileName, long? FileSize);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record SharedMediaDto(
|
||||
Guid Id,
|
||||
@@ -18,3 +18,4 @@ public record SharedMediaDto(
|
||||
string? Type,
|
||||
List<MediaDto>? Media
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record TogglePinResponse(bool IsPinned);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public sealed record UpdateChatRequest(string? Name, string? Description);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.DTOs;
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record UploadFileResponseDto(
|
||||
string Url,
|
||||
string Filename,
|
||||
long Size
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using global::Knot.Modules.Chats.Domain;
|
||||
using global::Knot.Modules.Chats.Infrastructure.SignalR;
|
||||
using global::Knot.Modules.Conversations.Domain;
|
||||
using global::Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using global::Knot.Shared.Kernel;
|
||||
using global::Knot.Modules.Chats.Application.Abstractions;
|
||||
using global::Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.Delete;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Delete;
|
||||
|
||||
public sealed record DeleteMessagesCommand(
|
||||
Guid ChatId,
|
||||
@@ -77,3 +79,5 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
|
||||
return global::Knot.Shared.Kernel.Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -5,12 +7,12 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.DTOs;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.GetMessages;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.GetMessages;
|
||||
|
||||
public record GetMessagesQuery(Guid UserId, Guid ChatId, string? Cursor) : IQuery<List<MessageDetailDto>>;
|
||||
|
||||
@@ -145,3 +147,5 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -6,12 +8,12 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.DTOs;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.GetSharedMedia;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.GetSharedMedia;
|
||||
|
||||
public record GetSharedMediaQuery(Guid UserId, Guid ChatId, string? Type) : IQuery<List<SharedMediaDto>>;
|
||||
|
||||
@@ -125,3 +127,5 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Infrastructure.SignalR;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.React;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.React;
|
||||
|
||||
public sealed record AddReactionCommand(
|
||||
Guid MessageId,
|
||||
@@ -70,3 +72,5 @@ public sealed class AddReactionCommandHandler : ICommandHandler<AddReactionComma
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Infrastructure.SignalR;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.React;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.React;
|
||||
|
||||
public sealed record RemoveReactionCommand(
|
||||
Guid MessageId,
|
||||
@@ -61,3 +63,5 @@ public sealed class RemoveReactionCommandHandler : ICommandHandler<RemoveReactio
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using MediatR;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.Read;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Read;
|
||||
|
||||
public sealed record ReadMessagesCommand(Guid ChatId, Guid UserId, Guid LastReadMessageId, long LastReadSequenceId) : ICommand;
|
||||
|
||||
@@ -33,3 +33,4 @@ public sealed class ReadMessagesCommandHandler : ICommandHandler<ReadMessagesCom
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -5,10 +7,10 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.DTOs;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.SearchMessages;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.SearchMessages;
|
||||
|
||||
public record SearchMessagesQuery(Guid UserId, string Query, Guid? ChatId) : IQuery<List<SearchMessageDto>>;
|
||||
|
||||
@@ -66,3 +68,5 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler<SearchMessagesQ
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.Send;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Send;
|
||||
|
||||
/// <summary>
|
||||
/// Команда для отправки сообщения в чат.
|
||||
@@ -133,3 +135,5 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
@@ -7,9 +7,9 @@ using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Chats.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
namespace Knot.Modules.Chats.Application.Messages.UploadFile;
|
||||
namespace Knot.Modules.Conversations.Application.Messages.UploadFile;
|
||||
|
||||
public record UploadFileCommand(string FileName, string ContentType, long Length, Stream FileStream) : ICommand<UploadFileResponseDto>;
|
||||
|
||||
@@ -42,3 +42,4 @@ internal sealed class UploadFileCommandHandler : ICommandHandler<UploadFileComma
|
||||
return Result.Success(new UploadFileResponseDto("/api/files/" + fileId, request.FileName, request.Length));
|
||||
}
|
||||
}
|
||||
|
||||
43
backend/src/Modules/Conversations/DependencyInjection.cs
Normal file
43
backend/src/Modules/Conversations/DependencyInjection.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Conversations;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// ╨а╨╡╨│╨╕╤Б╤В╤А╨░╤Ж╨╕╤П ╤Б╨╡╤А╨▓╨╕╤Б╨╛╨▓ ╨╝╨╛╨┤╤Г╨╗╤П Chats.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddConversationsModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// в•ЁР╨░╤Б╤В╤А╨╛╨╣╨║╨░ ╨▒╨░╨╖╤Л ╨┤╨░╨╜╨╜╤Л╤Е
|
||||
string connectionString = configuration.GetConnectionString("DefaultConnection")!;
|
||||
|
||||
services.AddDbContext<ChatsDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// MongoDB Setup for Messages
|
||||
|
||||
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
|
||||
|
||||
// Registration
|
||||
services.AddScoped<IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
|
||||
services.AddScoped<IChatRepository, ChatRepository>();
|
||||
|
||||
// MediatR
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
services.AddScoped<Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider, Knot.Modules.Conversations.Infrastructure.Services.ChatAccessProvider>();
|
||||
services.AddScoped<Knot.Modules.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
namespace Knot.Modules.Conversations.Domain;
|
||||
|
||||
public sealed record ChatCreatedDomainEvent(Chat Chat) : IDomainEvent;
|
||||
public sealed record ChatMemberAddedDomainEvent(Guid ChatId, Guid UserId) : IDomainEvent;
|
||||
@@ -154,3 +154,4 @@ public sealed class ChatMember : Entity<Guid>
|
||||
LastDeliveredMessageId = messageId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
namespace Knot.Modules.Conversations.Domain;
|
||||
|
||||
public static class ChatConstants
|
||||
{
|
||||
@@ -7,3 +7,4 @@ public static class ChatConstants
|
||||
public const int SearchMessagesLimit = 50;
|
||||
public const int MaxFileUploadSizeMb = 50;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
namespace Knot.Modules.Conversations.Domain;
|
||||
|
||||
public static class ChatErrors
|
||||
{
|
||||
@@ -19,3 +19,4 @@ public static class ChatErrors
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
namespace Knot.Modules.Conversations.Domain;
|
||||
|
||||
public interface IChatRepository
|
||||
{
|
||||
@@ -11,3 +11,4 @@ public interface IChatRepository
|
||||
Task<Chat?> GetFavoritesAsync(Guid userId, CancellationToken cancellationToken);
|
||||
Task<List<Chat>> GetUserChatsAsync(Guid userId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Chats.Infrastructure.SignalR;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Handlers;
|
||||
namespace Knot.Modules.Conversations.Infrastructure.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик доменного события создания чата.
|
||||
@@ -89,3 +89,4 @@ public sealed class ChatCreatedDomainEventHandler : INotificationHandler<ChatCre
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
||||
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
|
||||
public sealed class ChatRepository : IChatRepository
|
||||
{
|
||||
@@ -53,3 +53,4 @@ public sealed class ChatRepository : IChatRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
||||
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Контекст базы данных для модуля чатов.
|
||||
@@ -85,3 +85,4 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Knot.Modules.Conversations.Infrastructure.Services;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
public class ChatAccessProvider : IChatAccessProvider { private readonly ChatsDbContext _db; public ChatAccessProvider(ChatsDbContext db) { _db = db; } public Task<List<Guid>> GetValidChatIdsForUserAsync(Guid userId, CancellationToken ct) { return _db.Chats.Where(c => c.Members.Any(m => m.UserId == userId)).Select(c => c.Id).ToListAsync(ct); } }
|
||||
|
||||
@@ -4,15 +4,15 @@ using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Knot.Modules.Chats.Application.Messages.Send;
|
||||
using Knot.Modules.Chats.Application.Messages.Read;
|
||||
using Knot.Modules.Chats.Application.Messages.Delete;
|
||||
using Knot.Modules.Chats.Application.Messages.React;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Modules.Conversations.Application.Messages.Send;
|
||||
using Knot.Modules.Conversations.Application.Messages.Read;
|
||||
using Knot.Modules.Conversations.Application.Messages.Delete;
|
||||
using Knot.Modules.Conversations.Application.Messages.React;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.SignalR;
|
||||
namespace Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
|
||||
/// <summary>
|
||||
/// Хаб SignalR для обработки сообщений и WebRTC сигналинга в реальном времени.
|
||||
@@ -661,3 +661,4 @@ public sealed class ChatHub : Hub
|
||||
public record GroupRenegotiateAnswerRequest(string ChatId, string TargetUserId, object Answer);
|
||||
public record FriendSignalRequest(string FriendId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
public class MessageNotifier : IMessageNotifier { private readonly IHubContext<ChatHub> _hubContext; public MessageNotifier(IHubContext<ChatHub> hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } }
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
@@ -33,3 +35,4 @@
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -9,11 +9,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Migrations
|
||||
namespace Knot.Modules.Conversations.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChatsDbContext))]
|
||||
[Migration("20260320185319_AddHighWaterMark")]
|
||||
partial class AddHighWaterMark
|
||||
[Migration("20260322191932_InitialConversations")]
|
||||
partial class InitialConversations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -26,7 +26,7 @@ namespace Knot.Modules.Chats.Migrations
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -56,9 +56,9 @@ namespace Knot.Modules.Chats.Migrations
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 =>
|
||||
b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -3,10 +3,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Migrations
|
||||
namespace Knot.Modules.Conversations.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialChats : Migration
|
||||
public partial class InitialConversations : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
@@ -24,7 +24,8 @@ namespace Knot.Modules.Chats.Migrations
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
Avatar = table.Column<string>(type: "text", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
LastMessageSequenceId = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -42,7 +43,10 @@ namespace Knot.Modules.Chats.Migrations
|
||||
Role = table.Column<string>(type: "text", nullable: false),
|
||||
JoinedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsPinned = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsMuted = table.Column<bool>(type: "boolean", nullable: false)
|
||||
IsMuted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LastReadMessageId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
LastReadSequenceId = table.Column<long>(type: "bigint", nullable: false),
|
||||
LastDeliveredMessageId = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -8,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Migrations
|
||||
namespace Knot.Modules.Conversations.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChatsDbContext))]
|
||||
partial class ChatsDbContextModelSnapshot : ModelSnapshot
|
||||
@@ -23,7 +23,7 @@ namespace Knot.Modules.Chats.Migrations
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -53,9 +53,9 @@ namespace Knot.Modules.Chats.Migrations
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 =>
|
||||
b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -0,0 +1,174 @@
|
||||
using Carter;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Application.Chats.GetChats;
|
||||
using Knot.Modules.Conversations.Application.Chats.GetChatById;
|
||||
using Knot.Modules.Conversations.Application.Chats.Create;
|
||||
using Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites;
|
||||
using Knot.Modules.Conversations.Application.Chats.Update;
|
||||
using Knot.Modules.Conversations.Application.Chats.LeaveOrDelete;
|
||||
using Knot.Modules.Conversations.Application.Chats.Clear;
|
||||
using Knot.Modules.Conversations.Application.Chats.TogglePin;
|
||||
using Knot.Modules.Conversations.Application.Chats.Members;
|
||||
using Knot.Modules.Conversations.Application.Chats.Avatar;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Knot.Modules.Conversations.Presentation.Endpoints;
|
||||
|
||||
public sealed class ChatsEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("api/chats").RequireAuthorization();
|
||||
|
||||
group.MapGet("", async (ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetChatsQuery(userContext.UserId), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description);
|
||||
});
|
||||
|
||||
group.MapPost("", async ([FromBody] CreateChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var command = new CreateChatCommand(request.Name, request.Type, request.MemberIds);
|
||||
var result = await sender.Send(command, ct);
|
||||
if (result.IsFailure) return Results.BadRequest(result.Error.Description);
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
|
||||
group.MapPost("personal", async ([FromBody] CreatePersonalChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { userContext.UserId, request.UserId });
|
||||
var result = await sender.Send(command, ct);
|
||||
if (result.IsFailure) return Results.BadRequest(result.Error.Description);
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
|
||||
group.MapPost("group", async ([FromBody] CreateGroupChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var memberIds = request.MemberIds.ToList();
|
||||
if (memberIds.Contains(userContext.UserId)) memberIds.Remove(userContext.UserId);
|
||||
memberIds.Insert(0, userContext.UserId);
|
||||
|
||||
var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds);
|
||||
var result = await sender.Send(command, ct);
|
||||
if (result.IsFailure) return Results.BadRequest(result.Error.Description);
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
|
||||
group.MapPost("favorites", async (ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetOrCreateFavoritesCommand(userContext.UserId), ct);
|
||||
if (result.IsFailure) return Results.BadRequest(result.Error.Description);
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
|
||||
group.MapPut("{id:guid}", async (Guid id, [FromBody] UpdateChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new UpdateChatCommand(id, userContext.UserId, request.Name, request.Description), ct);
|
||||
if (result.IsFailure) return Results.NotFound();
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
|
||||
group.MapDelete("{id:guid}", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new LeaveOrDeleteChatCommand(id, userContext.UserId), ct);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
if (result.Error.Code == "Unauthorized") return Results.Forbid();
|
||||
return Results.NotFound();
|
||||
}
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("{id:guid}/clear", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new ClearChatCommand(id, userContext.UserId), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
|
||||
});
|
||||
|
||||
group.MapPost("{id:guid}/pin", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new TogglePinCommand(id, userContext.UserId), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
|
||||
});
|
||||
|
||||
group.MapPost("{id:guid}/members", async (Guid id, [FromBody] AddMembersRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new AddMembersCommand(id, userContext.UserId, request.UserIds.ToList()), ct);
|
||||
if (result.IsFailure) return Results.NotFound();
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
|
||||
group.MapDelete("{id:guid}/members/{userId:guid}", async (Guid id, Guid userId, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new RemoveMemberCommand(id, userContext.UserId, userId), ct);
|
||||
if (result.IsFailure) return Results.NotFound();
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
|
||||
group.MapPost("{id:guid}/avatar", async (Guid id, HttpRequest req, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
if (!req.HasFormContentType) return Results.BadRequest("No file");
|
||||
var form = await req.ReadFormAsync(ct);
|
||||
var avatar = form.Files.FirstOrDefault();
|
||||
if (avatar == null || avatar.Length == 0) return Results.BadRequest("No file");
|
||||
|
||||
using var stream = avatar.OpenReadStream();
|
||||
var result = await sender.Send(new UploadGroupAvatarCommand(id, userContext.UserId, avatar.FileName, avatar.ContentType, stream), ct);
|
||||
if (result.IsFailure) return Results.NotFound();
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
}).DisableAntiforgery();
|
||||
|
||||
group.MapPost("{id:guid}/avatar/crop", async (Guid id, HttpRequest req, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
if (!req.HasFormContentType) return Results.BadRequest("No file");
|
||||
var form = await req.ReadFormAsync(ct);
|
||||
var avatar = form.Files.FirstOrDefault();
|
||||
if (avatar == null || avatar.Length == 0) return Results.BadRequest("No file");
|
||||
|
||||
int.TryParse(form["x"], out int x);
|
||||
int.TryParse(form["y"], out int y);
|
||||
int.TryParse(form["width"], out int width);
|
||||
int.TryParse(form["height"], out int height);
|
||||
|
||||
using var stream = avatar.OpenReadStream();
|
||||
var result = await sender.Send(new CropGroupAvatarCommand(id, userContext.UserId, avatar.FileName ?? "avatar.jpg", avatar.ContentType, stream, x, y, width, height), ct);
|
||||
if (result.IsFailure) return Results.NotFound();
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
}).DisableAntiforgery();
|
||||
|
||||
group.MapDelete("{id:guid}/avatar", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new RemoveGroupAvatarCommand(id, userContext.UserId), ct);
|
||||
if (result.IsFailure) return Results.NotFound();
|
||||
|
||||
var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct);
|
||||
return Results.Ok(chatResult.Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using Carter;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Application.Messages.GetMessages;
|
||||
using Knot.Modules.Conversations.Application.Messages.SearchMessages;
|
||||
using Knot.Modules.Conversations.Application.Messages.UploadFile;
|
||||
using Knot.Modules.Conversations.Application.Messages.GetSharedMedia;
|
||||
using Knot.Modules.Conversations.Application.Messages.Send;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Knot.Modules.Conversations.Presentation.Endpoints;
|
||||
|
||||
public sealed class MessagesEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("api/messages").RequireAuthorization();
|
||||
|
||||
group.MapGet("chat/{chatId:guid}", async (Guid chatId, [FromQuery] string? cursor, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetMessagesQuery(userContext.UserId, chatId, cursor), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description);
|
||||
});
|
||||
|
||||
group.MapGet("search", async ([FromQuery] string q, [FromQuery] Guid? chatId, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new SearchMessagesQuery(userContext.UserId, q, chatId), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description);
|
||||
});
|
||||
|
||||
group.MapPost("upload", async (HttpRequest req, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
if (!req.HasFormContentType) return Results.BadRequest("No file uploaded");
|
||||
var form = await req.ReadFormAsync(ct);
|
||||
var file = form.Files.FirstOrDefault();
|
||||
if (file == null || file.Length == 0) return Results.BadRequest("No file uploaded");
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var result = await sender.Send(new UploadFileCommand(file.FileName, file.ContentType, file.Length, stream), ct);
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
if (result.Error.Code == "File.TooLarge") return Results.StatusCode(413);
|
||||
return Results.BadRequest(result.Error.Description);
|
||||
}
|
||||
return Results.Ok(result.Value);
|
||||
}).DisableAntiforgery();
|
||||
|
||||
group.MapGet("chat/{chatId:guid}/shared", async (Guid chatId, [FromQuery] string? type, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetSharedMediaQuery(userContext.UserId, chatId, type), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description);
|
||||
});
|
||||
|
||||
group.MapPost("chat/{chatId:guid}", async (Guid chatId, [FromBody] SendMessageRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
var attachments = request.Attachments?.Select(a =>
|
||||
new AttachmentRequest(a.Type, a.Url, a.FileName, a.FileSize)).ToList();
|
||||
|
||||
var command = new SendMessageCommand(
|
||||
chatId,
|
||||
userContext.UserId,
|
||||
request.Content,
|
||||
request.Type,
|
||||
attachments,
|
||||
request.ReplyToId,
|
||||
request.Quote,
|
||||
request.ForwardedFromId);
|
||||
|
||||
var result = await sender.Send(command, ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
using Knot.Modules.Stories.Domain;
|
||||
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
|
||||
namespace Host.Application.Federation.Commands;
|
||||
|
||||
public record HandshakeRequest(string Domain, string PublicKey);
|
||||
public record HandshakeResponse(string Domain, string PublicKey, string Status);
|
||||
|
||||
public record HandshakeFederationCommand(HandshakeRequest Request) : ICommand<HandshakeResponse>;
|
||||
|
||||
internal sealed class HandshakeFederationCommandHandler : ICommandHandler<HandshakeFederationCommand, HandshakeResponse>
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
|
||||
public HandshakeFederationCommandHandler(ISettingsService settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public Task<Result<HandshakeResponse>> Handle(HandshakeFederationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var conf = _settings.Current;
|
||||
if (!conf.EnableConfederation)
|
||||
return Task.FromResult(Result.Failure<HandshakeResponse>(new Error(Errors.DisabledByAdmin, "Federation is disabled")));
|
||||
|
||||
if (string.IsNullOrEmpty(request.Request.Domain) || string.IsNullOrEmpty(request.Request.PublicKey))
|
||||
return Task.FromResult(Result.Failure<HandshakeResponse>(DomainErrors.RequestInvalid));
|
||||
|
||||
var allowedList = conf.AllowedDomains?.Select(d => d.Trim().ToLower()).ToList() ?? new System.Collections.Generic.List<string>();
|
||||
if (!allowedList.Contains(request.Request.Domain.ToLowerInvariant()))
|
||||
return Task.FromResult(Result.Failure<HandshakeResponse>(StoryErrors.Unauthorized));
|
||||
|
||||
using var rsa = RSA.Create(2048);
|
||||
var selfPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
|
||||
|
||||
var response = new HandshakeResponse(
|
||||
Environment.GetEnvironmentVariable("DOMAIN") ?? "knot.local",
|
||||
selfPublicKey,
|
||||
"Accepted"
|
||||
);
|
||||
|
||||
return Task.FromResult(Result.Success(response));
|
||||
}
|
||||
}
|
||||
7
backend/src/Modules/Federation/DependencyInjection.cs
Normal file
7
backend/src/Modules/Federation/DependencyInjection.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Modules.Federation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
public static class DependencyInjection {
|
||||
public static IServiceCollection AddFederationModule(this IServiceCollection services) {
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user