Compare commits
68
Commits
cb93ff7240
...
bugfix_web
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4f61071ed | ||
|
|
e8c9a55fe9 | ||
|
|
bccbf12a11 | ||
|
|
c8b0384392 | ||
|
|
5245e8b7ae | ||
|
|
b346372555 | ||
|
|
d2e6eb578a | ||
|
|
ae710c3d43 | ||
|
|
600e43eec5 | ||
|
|
7b251686b2 | ||
|
|
00682b2977 | ||
|
|
e05572fc3f | ||
|
|
c264b7df27 | ||
|
|
56f75ae32b | ||
|
|
ca9cf27716 | ||
|
|
7225e3272e | ||
|
|
86ae06beb6 | ||
|
|
454f70f716 | ||
|
|
e700609d30 | ||
|
|
88f39aa51f | ||
|
|
c3dbbaa7b8 | ||
|
|
2eb4f48ca0 | ||
|
|
0c1adaab6c | ||
|
|
a52726d0e6 | ||
|
|
d812a7a40c | ||
|
|
c8b4fed25a | ||
|
|
8399d32490 | ||
|
|
3905094ff4 | ||
|
|
9e8625aea1 | ||
|
|
5b905c94da | ||
|
|
0eecc01374 | ||
|
|
c37e1723d4 | ||
|
|
02043d4d97 | ||
|
|
852efa090e | ||
|
|
c45f4db61c | ||
|
|
02a85fc587 | ||
|
|
e09860700c | ||
|
|
32c9bc43cf | ||
|
|
d96e4ec7d4 | ||
|
|
1558b20470 | ||
|
|
fa185afc73 | ||
|
|
90096ce2bc | ||
|
|
65d5f5fee9 | ||
|
|
e11240f78f | ||
|
|
53f193970c | ||
|
|
f41ad0bcf8 | ||
|
|
b49ff47762 | ||
|
|
182bbe2ad8 | ||
|
|
a04f04a448 | ||
|
|
2ab5b295e8 | ||
|
|
1b8abbc995 | ||
|
|
943699139b | ||
|
|
757142eda7 | ||
|
|
a42007df2d | ||
|
|
9df7d7aaf1 | ||
|
|
4ae7dd60ce | ||
|
|
249c344df8 | ||
|
|
2c6d6f831f | ||
|
|
bce8f92101 | ||
|
|
aa69c44a64 | ||
|
|
4faa7561a0 | ||
|
|
8025340e45 | ||
|
|
60d3cf12ae | ||
|
|
b09e197626 | ||
|
|
f1f8f6e012 | ||
|
|
d3f1e3f361 | ||
|
|
ce212c11c1 | ||
|
|
143c0b7dc4 |
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Shared\IntegrationTests.Shared\Knot.IntegrationTests.Shared.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Admin\Knot.Modules.Admin.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Admin\Knot.Modules.Admin.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Knot.Modules.Admin.Application.Admin.Commands;
|
||||||
|
using Knot.Modules.Auth.Application.Abstractions;
|
||||||
|
using Knot.Modules.Auth.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Admin.UnitTests;
|
||||||
|
|
||||||
|
public class ResetUserPasswordCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IAuthUnitOfWork _authUnitOfWork;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ResetUserPasswordCommandHandler _handler;
|
||||||
|
|
||||||
|
public ResetUserPasswordCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_authUnitOfWork = Substitute.For<IAuthUnitOfWork>();
|
||||||
|
_userRepository = Substitute.For<IUserRepository>();
|
||||||
|
_handler = new ResetUserPasswordCommandHandler(_userRepository, _authUnitOfWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnError_WhenUserNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new ResetUserPasswordCommand(Guid.NewGuid(), "NewP@ssw0rd");
|
||||||
|
_userRepository.GetByIdAsync(command.UserId, Arg.Any<CancellationToken>()).Returns((User?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Should().Be(AuthErrors.UserNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldSucceed_WhenUserFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = User.Create("User", "pass", "salt", "admin");
|
||||||
|
var command = new ResetUserPasswordCommand(user.Id, "NewP@ssw0rd");
|
||||||
|
|
||||||
|
_userRepository.GetByIdAsync(command.UserId, Arg.Any<CancellationToken>()).Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
await _authUnitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Auth\Knot.Modules.Auth.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using NSubstitute;
|
||||||
|
using Knot.Modules.Auth.Application.Abstractions;
|
||||||
|
using Knot.Modules.Auth.Application.Users.Login;
|
||||||
|
using Knot.Modules.Auth.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Modules.Auth.Application.Users.Auth;
|
||||||
|
using Xunit;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.UnitTests;
|
||||||
|
|
||||||
|
public class LoginUserCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IJwtTokenProvider _tokenProvider;
|
||||||
|
private readonly LoginUserCommandHandler _handler;
|
||||||
|
|
||||||
|
public LoginUserCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_userRepository = Substitute.For<IUserRepository>();
|
||||||
|
_tokenProvider = Substitute.For<IJwtTokenProvider>();
|
||||||
|
_handler = new LoginUserCommandHandler(_userRepository, _tokenProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnToken_WhenCredentialsAreValid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var password = "password123";
|
||||||
|
var passwordHash = BCrypt.Net.BCrypt.HashPassword(password);
|
||||||
|
var user = User.Create("testuser", passwordHash, "Test User", null, null);
|
||||||
|
|
||||||
|
var command = new LoginUserCommand("testuser", password);
|
||||||
|
|
||||||
|
_userRepository.GetByUsernameAsync(command.Username, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
_tokenProvider.Generate(user).Returns("valid-jwt-token");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Token.Should().Be("valid-jwt-token");
|
||||||
|
result.Value.User.Username.Should().Be("testuser");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnFailure_WhenUserDoesNotExist()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new LoginUserCommand("nonexistent", "password123");
|
||||||
|
_userRepository.GetByUsernameAsync(command.Username, Arg.Any<CancellationToken>())
|
||||||
|
.Returns((User)null!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Code.Should().Be(AuthErrors.IdentityInvalidCredentials.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnFailure_WhenPasswordIsInvalid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var correctPassword = "correctPassword";
|
||||||
|
var passwordHash = BCrypt.Net.BCrypt.HashPassword(correctPassword);
|
||||||
|
var user = User.Create("testuser", passwordHash, "Test User", null, null);
|
||||||
|
|
||||||
|
var command = new LoginUserCommand("testuser", "wrongPassword");
|
||||||
|
|
||||||
|
_userRepository.GetByUsernameAsync(command.Username, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Code.Should().Be(AuthErrors.IdentityInvalidCredentials.Code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Modules.Conversations.Domain;
|
||||||
|
using Knot.Modules.Messaging.Domain;
|
||||||
|
using Knot.Modules.Conversations.Application.Abstractions;
|
||||||
|
using Knot.Modules.Conversations.Application.Chats.Create;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.UnitTests.Chats;
|
||||||
|
|
||||||
|
public class CreateChatCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IChatRepository _chatRepository;
|
||||||
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
|
private readonly CreateChatCommandHandler _handler;
|
||||||
|
|
||||||
|
public CreateChatCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_chatRepository = Substitute.For<IChatRepository>();
|
||||||
|
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
||||||
|
_handler = new CreateChatCommandHandler(_chatRepository, _unitOfWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldCreateChatAndAddMembers()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new CreateChatCommand("Test Group", ChatType.Group, new List<Guid> { Guid.NewGuid(), Guid.NewGuid() });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(request, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().NotBeEmpty();
|
||||||
|
|
||||||
|
_chatRepository.Received(1).Add(Arg.Is<Chat>(c =>
|
||||||
|
c.Name == "Test Group" &&
|
||||||
|
c.Type == ChatType.Group &&
|
||||||
|
c.Members.Count == 2 &&
|
||||||
|
c.Members.First().Role == ChatRole.Owner &&
|
||||||
|
c.Members.Last().Role == ChatRole.Member));
|
||||||
|
|
||||||
|
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Modules.Conversations.Domain;
|
||||||
|
using Knot.Modules.Messaging.Domain;
|
||||||
|
using Knot.Modules.Conversations.Application.Abstractions;
|
||||||
|
using Knot.Modules.Conversations.Application.Chats.GetChats;
|
||||||
|
using Knot.Modules.Conversations.Application.DTOs;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.UnitTests.Chats;
|
||||||
|
|
||||||
|
public class GetChatsQueryHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IChatRepository _chatRepository;
|
||||||
|
private readonly IUserDisplayNameProvider _userProvider;
|
||||||
|
private readonly IMessageRepository _messageRepository;
|
||||||
|
private readonly IMessageReactionRepository _reactionRepository;
|
||||||
|
private readonly GetChatsQueryHandler _handler;
|
||||||
|
|
||||||
|
public GetChatsQueryHandlerTests()
|
||||||
|
{
|
||||||
|
_chatRepository = Substitute.For<IChatRepository>();
|
||||||
|
_userProvider = Substitute.For<IUserDisplayNameProvider>();
|
||||||
|
_messageRepository = Substitute.For<IMessageRepository>();
|
||||||
|
_reactionRepository = Substitute.For<IMessageReactionRepository>();
|
||||||
|
|
||||||
|
_handler = new GetChatsQueryHandler(_chatRepository, _userProvider, _messageRepository, _reactionRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnUserChats_WhenTheyExist()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
var request = new GetChatsQuery(userId);
|
||||||
|
|
||||||
|
var chat1 = Chat.Create("Test Chat", ChatType.Group);
|
||||||
|
chat1.GetType().GetProperty("Id")?.SetValue(chat1, Guid.NewGuid());
|
||||||
|
chat1.AddMember(userId, ChatRole.Owner);
|
||||||
|
|
||||||
|
var chat2 = Chat.Create("Personal", ChatType.Personal);
|
||||||
|
chat2.GetType().GetProperty("Id")?.SetValue(chat2, Guid.NewGuid());
|
||||||
|
chat2.AddMember(userId, ChatRole.Member);
|
||||||
|
|
||||||
|
_chatRepository.GetUserChatsAsync(userId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new List<Chat> { chat1, chat2 });
|
||||||
|
|
||||||
|
_messageRepository.GetChatMessagesAsync(Arg.Any<Guid>(), 1, 0, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new List<Message>());
|
||||||
|
|
||||||
|
_userProvider.GetUsersInfoAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<Guid, UserInfo>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(request, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().NotBeNull();
|
||||||
|
|
||||||
|
// It always appends synthetic "favorites" chat at the end if not found
|
||||||
|
result.Value.Count.Should().Be(2);
|
||||||
|
result.Value.Any(c => c.Name == "Test Chat").Should().BeTrue();
|
||||||
|
result.Value.Any(c => c.Type == "favorites").Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using NSubstitute;
|
||||||
|
using Knot.Modules.Conversations.Application.Abstractions;
|
||||||
|
using Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites;
|
||||||
|
using Knot.Modules.Conversations.Domain;
|
||||||
|
using Knot.Modules.Messaging.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.UnitTests;
|
||||||
|
|
||||||
|
public class GetOrCreateFavoritesCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IChatRepository _chatRepository;
|
||||||
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
|
private readonly GetOrCreateFavoritesCommandHandler _handler;
|
||||||
|
|
||||||
|
public GetOrCreateFavoritesCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_chatRepository = Substitute.For<IChatRepository>();
|
||||||
|
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
||||||
|
_handler = new GetOrCreateFavoritesCommandHandler(_chatRepository, _unitOfWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnExistingChat_WhenFavoritesAlreadyExists()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
var existingChat = Chat.Create("Избранное", ChatType.Favorites);
|
||||||
|
|
||||||
|
_chatRepository.GetFavoritesAsync(userId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(existingChat);
|
||||||
|
|
||||||
|
var command = new GetOrCreateFavoritesCommand(userId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().Be(existingChat.Id);
|
||||||
|
|
||||||
|
_chatRepository.DidNotReceive().Add(Arg.Any<Chat>());
|
||||||
|
await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldCreateNewChat_WhenFavoritesDoesNotExist()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
_chatRepository.GetFavoritesAsync(userId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns((Chat)null!);
|
||||||
|
|
||||||
|
var command = new GetOrCreateFavoritesCommand(userId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().NotBeEmpty();
|
||||||
|
|
||||||
|
_chatRepository.Received(1).Add(Arg.Is<Chat>(c =>
|
||||||
|
c.Type == ChatType.Favorites &&
|
||||||
|
c.Name == "Избранное" &&
|
||||||
|
c.Members.Any(m => m.UserId == userId)));
|
||||||
|
|
||||||
|
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Chats\Knot.Modules.Conversations.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Messaging\Knot.Modules.Messaging.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Conversations\Knot.Modules.Conversations.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using MediatR;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Modules.Conversations.Domain;
|
||||||
|
using Knot.Modules.Messaging.Domain;
|
||||||
|
using Knot.Modules.Conversations.Application.Abstractions;
|
||||||
|
using Knot.Modules.Conversations.Application.Messages.Send;
|
||||||
|
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||||
|
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.UnitTests.Messages;
|
||||||
|
|
||||||
|
public class SendMessageCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IChatRepository _chatRepository;
|
||||||
|
private readonly IMessageRepository _messageRepository;
|
||||||
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
private readonly IMessagesSettings _messagesSettings;
|
||||||
|
private readonly SendMessageCommandHandler _handler;
|
||||||
|
|
||||||
|
public SendMessageCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_chatRepository = Substitute.For<IChatRepository>();
|
||||||
|
_messageRepository = Substitute.For<IMessageRepository>();
|
||||||
|
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
||||||
|
_mediator = Substitute.For<IMediator>();
|
||||||
|
_messagesSettings = Substitute.For<IMessagesSettings>();
|
||||||
|
|
||||||
|
var config = new Knot.Modules.Settings.Application.Settings.DTOs.MessagesConfig();
|
||||||
|
_messagesSettings.Current.Returns(config);
|
||||||
|
|
||||||
|
_handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork, _mediator, _messagesSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnFailure_WhenChatNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new SendMessageCommand(Guid.NewGuid(), Guid.NewGuid(), "Hello", "text");
|
||||||
|
_chatRepository.GetByIdAsync(command.ChatId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns((Chat)null!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Code.Should().Be(ChatErrors.ChatsNotFound.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnFailure_WhenSenderIsNotMember()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var chat = Chat.Create("Test", ChatType.Group);
|
||||||
|
var command = new SendMessageCommand(chat.Id, Guid.NewGuid(), "Hello", "text");
|
||||||
|
|
||||||
|
_chatRepository.GetByIdAsync(command.ChatId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(chat);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Code.Should().Be(ChatErrors.ChatsForbidden.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldCreateMessage_WhenAuthorized()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var senderId = Guid.NewGuid();
|
||||||
|
var chat = Chat.Create("Test", ChatType.Group);
|
||||||
|
chat.AddMember(senderId, ChatRole.Member);
|
||||||
|
|
||||||
|
var command = new SendMessageCommand(chat.Id, senderId, "Hello", "text");
|
||||||
|
|
||||||
|
_chatRepository.GetByIdAsync(command.ChatId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(chat);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().NotBeEmpty();
|
||||||
|
|
||||||
|
_messageRepository.Received(1).Add(Arg.Is<Message>(m =>
|
||||||
|
m.ChatId == chat.Id &&
|
||||||
|
m.SenderId == senderId &&
|
||||||
|
m.Content == "Hello" &&
|
||||||
|
m.Type == "text"));
|
||||||
|
|
||||||
|
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Federation\Knot.Modules.Federation.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Klipy\Knot.Modules.Klipy.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Relations\Knot.Modules.Relations.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Testcontainers.PostgreSql;
|
||||||
|
using Testcontainers.MongoDb;
|
||||||
|
using Xunit;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Knot.IntegrationTests;
|
||||||
|
|
||||||
|
public abstract class BaseIntegrationTest : IAsyncLifetime
|
||||||
|
{
|
||||||
|
protected readonly PostgreSqlContainer _postgresContainer = new PostgreSqlBuilder()
|
||||||
|
.WithImage("postgres:15-alpine")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
protected readonly MongoDbContainer _mongoContainer = new MongoDbBuilder()
|
||||||
|
.WithImage("mongo:6.0")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
protected HttpClient _client;
|
||||||
|
protected WebApplicationFactory<Program> _factory;
|
||||||
|
protected readonly Guid _userId = Guid.NewGuid();
|
||||||
|
protected readonly IUserContext _userContextMock = Substitute.For<IUserContext>();
|
||||||
|
|
||||||
|
public virtual async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
await _postgresContainer.StartAsync();
|
||||||
|
await _mongoContainer.StartAsync();
|
||||||
|
|
||||||
|
_userContextMock.UserId.Returns(_userId);
|
||||||
|
_userContextMock.IsAuthenticated.Returns(true);
|
||||||
|
|
||||||
|
_factory = new IntegrationTestWebApplicationFactory(
|
||||||
|
_postgresContainer.GetConnectionString(),
|
||||||
|
_mongoContainer.GetConnectionString(),
|
||||||
|
_userContextMock);
|
||||||
|
|
||||||
|
_client = _factory.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _postgresContainer.StopAsync();
|
||||||
|
await _mongoContainer.StopAsync();
|
||||||
|
_factory?.Dispose();
|
||||||
|
_client?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IntegrationTestWebApplicationFactory : WebApplicationFactory<Program>
|
||||||
|
{
|
||||||
|
private readonly string _pgConnectionString;
|
||||||
|
private readonly string _mongoConnectionString;
|
||||||
|
private readonly IUserContext _userContext;
|
||||||
|
|
||||||
|
public IntegrationTestWebApplicationFactory(string pg, string mongo, IUserContext userContext)
|
||||||
|
{
|
||||||
|
_pgConnectionString = pg;
|
||||||
|
_mongoConnectionString = mongo;
|
||||||
|
_userContext = userContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
|
{
|
||||||
|
builder.ConfigureAppConfiguration((context, config) =>
|
||||||
|
{
|
||||||
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["ConnectionStrings:DefaultConnection"] = _pgConnectionString,
|
||||||
|
["ConnectionStrings:MongoConnection"] = _mongoConnectionString,
|
||||||
|
["DATABASE_URL"] = _pgConnectionString,
|
||||||
|
["MONGO_CONNECTION"] = _mongoConnectionString,
|
||||||
|
["KNOT_MASTER_ENCRYPTION_KEY"] = "TestEncryptionKey_32CharactersLong!"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.ConfigureTestServices(services => {
|
||||||
|
services.AddScoped(_ => _userContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="Respawn" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Testcontainers.MongoDb" Version="4.11.0" />
|
||||||
|
<PackageReference Include="Testcontainers.PostgreSql" Version="4.11.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Host\Host.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Storage\Knot.Modules.Storage.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Shared\IntegrationTests.Shared\Knot.IntegrationTests.Shared.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Stories\Knot.Modules.Stories.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Knot.IntegrationTests;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Knot.Modules.Stories.Application.Stories.Commands.CreateStory;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NSubstitute;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Xunit;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Knot.IntegrationTests.Stories;
|
||||||
|
|
||||||
|
public class StoriesTests : BaseIntegrationTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateStory_ShouldReturnOk_WhenValidRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new CreateStoryRequest("Text", null, "Hello Integration Test", null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await _client.PostAsJsonAsync("api/stories", request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
|
|
||||||
|
var content = await response.Content.ReadFromJsonAsync<dynamic>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateStoryRequest(string Type, string? MediaUrl, string? Content, string? BgColor);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Knot.Modules.Stories.Application.Stories.Commands.CreateStory;
|
||||||
|
using Knot.Modules.Stories.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||||
|
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||||
|
using Knot.Modules.Stories.Application.Abstractions;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Stories.UnitTests;
|
||||||
|
|
||||||
|
public class CreateStoryCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IStoryRepository _storyRepository;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly CreateStoryCommandHandler _handler;
|
||||||
|
|
||||||
|
public CreateStoryCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_storyRepository = Substitute.For<IStoryRepository>();
|
||||||
|
_settingsService = Substitute.For<ISettingsService>();
|
||||||
|
_handler = new CreateStoryCommandHandler(_storyRepository, _settingsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldSucceed_WhenValidRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new CreateStoryCommand(Guid.NewGuid(), "Text", null, "Hello Story", null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
await _storyRepository.Received(1).AddAsync(Arg.Any<Story>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldFail_WhenKlipyDisabledAndKlipyType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new CreateStoryCommand(Guid.NewGuid(), "Klipy", "http://klipy.com/gif", null, null);
|
||||||
|
var settings = new SystemSettingsDto { Klipy = new KlipyConfig { Enabled = false } };
|
||||||
|
_settingsService.GetSettingsAsync(Arg.Any<CancellationToken>()).Returns(settings);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Code.Should().Be("Stories.KlipyDisabled");
|
||||||
|
await _storyRepository.DidNotReceive().AddAsync(Arg.Any<Story>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldSucceed_WhenKlipyEnabledAndKlipyType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new CreateStoryCommand(Guid.NewGuid(), "Klipy", "http://klipy.com/gif", null, null);
|
||||||
|
var settings = new SystemSettingsDto { Klipy = new KlipyConfig { Enabled = true } };
|
||||||
|
_settingsService.GetSettingsAsync(Arg.Any<CancellationToken>()).Returns(settings);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
await _storyRepository.Received(1).AddAsync(Arg.Any<Story>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Knot.Modules.Stories.Application.Stories.Commands.DeleteStory;
|
||||||
|
using Knot.Modules.Stories.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Modules.Stories.Application.Abstractions;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Stories.UnitTests;
|
||||||
|
|
||||||
|
public class DeleteStoryCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IStoryRepository _storyRepository;
|
||||||
|
private readonly DeleteStoryCommandHandler _handler;
|
||||||
|
|
||||||
|
public DeleteStoryCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_storyRepository = Substitute.For<IStoryRepository>();
|
||||||
|
_handler = new DeleteStoryCommandHandler(_storyRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnError_WhenStoryNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new DeleteStoryCommand(Guid.NewGuid(), Guid.NewGuid());
|
||||||
|
_storyRepository.GetByIdAsync(command.StoryId, Arg.Any<CancellationToken>()).Returns((Story?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Should().Be(StoryErrors.StoryNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnError_WhenUserIsNotOwner()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ownerId = Guid.NewGuid();
|
||||||
|
var viewerId = Guid.NewGuid();
|
||||||
|
var story = Story.Create(ownerId, StoryType.Text, null, "Content", null);
|
||||||
|
var command = new DeleteStoryCommand(viewerId, story.Id);
|
||||||
|
|
||||||
|
_storyRepository.GetByIdAsync(command.StoryId, Arg.Any<CancellationToken>()).Returns(story);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
result.Error.Should().Be(StoryErrors.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldSucceed_WhenOwnerDeletes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ownerId = Guid.NewGuid();
|
||||||
|
var story = Story.Create(ownerId, StoryType.Text, null, "Content", null);
|
||||||
|
var command = new DeleteStoryCommand(ownerId, story.Id);
|
||||||
|
|
||||||
|
_storyRepository.GetByIdAsync(command.StoryId, Arg.Any<CancellationToken>()).Returns(story);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
await _storyRepository.Received(1).DeleteAsync(story, Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Stories\Knot.Modules.Stories.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\TelegramImport\Knot.Modules.TelegramImport.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Profiles\Knot.Modules.Profiles.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Knot.Modules.Profiles.Application.Profiles.UpdateProfile;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Knot.Contracts.Profiles.Domain;
|
||||||
|
using Knot.Contracts.Profiles.Application.DTOs;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Profiles.UnitTests;
|
||||||
|
|
||||||
|
public class UpdateProfileCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly IProfileRepository _profileRepository;
|
||||||
|
private readonly UpdateProfileCommandHandler _handler;
|
||||||
|
|
||||||
|
public UpdateProfileCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_profileRepository = Substitute.For<IProfileRepository>();
|
||||||
|
_handler = new UpdateProfileCommandHandler(_profileRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ShouldReturnError_WhenProfileNotFound()
|
||||||
|
{
|
||||||
|
var command = new UpdateProfileCommand(Guid.NewGuid(), "FirstName", "Bio", null, null);
|
||||||
|
_profileRepository.GetAsync(command.UserId, Arg.Any<CancellationToken>()).Returns((UserProfileDto?)null);
|
||||||
|
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsFailure.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\WebRtc\Knot.Modules.WebRtc.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Auth.Application.Abstractions;
|
||||||
|
|
||||||
|
public record GetUsersExistenceQuery(List<Guid> UserIds) : IQuery<List<Guid>>;
|
||||||
@@ -8,4 +8,7 @@ public static class AuthErrors
|
|||||||
public static Error IdentityRegistrationDisabled => new("Auth.RegistrationDisabled", "Registration is disabled");
|
public static Error IdentityRegistrationDisabled => new("Auth.RegistrationDisabled", "Registration is disabled");
|
||||||
public static Error IdentityUsernameNotUnique => new("Auth.UsernameNotUnique", "Username is already taken");
|
public static Error IdentityUsernameNotUnique => new("Auth.UsernameNotUnique", "Username is already taken");
|
||||||
public static Error UserNotFound => new("Auth.UserNotFound", "User not found");
|
public static Error UserNotFound => new("Auth.UserNotFound", "User not found");
|
||||||
|
public static Error PasswordConfirmationMismatch => new("Auth.PasswordConfirmationMismatch", "Passwords do not match");
|
||||||
|
public static Error PasswordTooShort => new("Auth.PasswordTooShort", "Password must be at least 8 characters");
|
||||||
|
public static Error OldPasswordInvalid => new("Auth.OldPasswordInvalid", "Current password is incorrect");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Knot.Contracts.Auth.Application.Auth.DTOs;
|
namespace Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||||
|
|
||||||
public class AuthResponseDto
|
public class AuthResponseDto
|
||||||
{
|
{
|
||||||
@@ -7,6 +7,7 @@ public class AuthResponseDto
|
|||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
public string? DisplayName { get; set; }
|
public string? DisplayName { get; set; }
|
||||||
|
public string? Avatar { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ResetPasswordDto
|
public class ResetPasswordDto
|
||||||
|
|||||||
@@ -10,9 +10,4 @@ public interface IChatQueryService
|
|||||||
Task<List<ChatInfo>> GetAllChatsAsync(CancellationToken cancellationToken);
|
Task<List<ChatInfo>> GetAllChatsAsync(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IUserDeleterService
|
|
||||||
{
|
|
||||||
Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ChatInfo(Guid Id, string? Avatar);
|
public record ChatInfo(Guid Id, string? Avatar);
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
namespace Knot.Modules.Conversations.Application.Abstractions;
|
namespace Knot.Contracts.Conversations.Abstractions;
|
||||||
|
|
||||||
public interface IUserDeleterService
|
public interface IUserDeleterService
|
||||||
{
|
{
|
||||||
@@ -36,17 +36,21 @@ public sealed class Chat : AggregateRoot<Guid>
|
|||||||
public string? Avatar { get; private set; }
|
public string? Avatar { get; private set; }
|
||||||
public DateTime CreatedAt { get; private set; }
|
public DateTime CreatedAt { get; private set; }
|
||||||
public long LastMessageSequenceId { get; private set; }
|
public long LastMessageSequenceId { get; private set; }
|
||||||
|
public bool IsImporting { get; private set; }
|
||||||
|
public Guid? ImportJobId { get; private set; }
|
||||||
|
|
||||||
private readonly List<ChatMember> _members = new();
|
private readonly List<ChatMember> _members = new();
|
||||||
public IReadOnlyCollection<ChatMember> Members => _members.AsReadOnly();
|
public IReadOnlyCollection<ChatMember> Members => _members.AsReadOnly();
|
||||||
|
|
||||||
private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id)
|
private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null, bool isImporting = false, Guid? importJobId = null) : base(id)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
Name = name;
|
Name = name;
|
||||||
Avatar = avatar;
|
Avatar = avatar;
|
||||||
Description = description;
|
Description = description;
|
||||||
CreatedAt = DateTime.UtcNow;
|
CreatedAt = DateTime.UtcNow;
|
||||||
|
IsImporting = isImporting;
|
||||||
|
ImportJobId = importJobId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Chat CreatePersonal()
|
public static Chat CreatePersonal()
|
||||||
@@ -63,13 +67,18 @@ public sealed class Chat : AggregateRoot<Guid>
|
|||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null)
|
public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null, bool isImporting = false, Guid? importJobId = null)
|
||||||
{
|
{
|
||||||
var chat = new Chat(Guid.NewGuid(), type, name, avatar, description);
|
var chat = new Chat(Guid.NewGuid(), type, name, avatar, description, isImporting, importJobId);
|
||||||
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
|
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
|
||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CompleteImport()
|
||||||
|
{
|
||||||
|
IsImporting = false;
|
||||||
|
}
|
||||||
|
|
||||||
public void AddMember(Guid userId, string role = "member")
|
public void AddMember(Guid userId, string role = "member")
|
||||||
{
|
{
|
||||||
if (_members.Any(m => m.UserId == userId))
|
if (_members.Any(m => m.UserId == userId))
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Knot.Contracts.Conversations.Domain;
|
||||||
|
|
||||||
|
public static class ChatConstants
|
||||||
|
{
|
||||||
|
public const int DefaultMessageQueryLimit = 50;
|
||||||
|
public const int MaxSharedMediaQueryLimit = 1000;
|
||||||
|
public const int MaxGroupNameLength = 100;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Conversations.Domain;
|
||||||
|
|
||||||
|
public static class ChatErrors
|
||||||
|
{
|
||||||
|
public static readonly Error ChatNotFound = new Error("Chat.NotFound", "Чат не найден");
|
||||||
|
public static readonly Error OnlyOwnerCanUpdate = new Error("Chat.OnlyOwnerCanUpdate", "Только владелец может редактировать чат");
|
||||||
|
public static readonly Error Unauthorized = new Error("Chat.Unauthorized", "Нет доступа к этому чату");
|
||||||
|
public static readonly Error FoldersDisabled = new Error("Chat.FoldersDisabled", "Папки отключены");
|
||||||
|
public static readonly Error FileEmpty = new Error("Chat.FileEmpty", "Файл пуст");
|
||||||
|
public static Error FileTooLarge(long maxMb) => new Error("Chat.FileTooLarge", $"Файл слишком большой (максимум {maxMb} МБ)");
|
||||||
|
public static readonly Error ChatsNotFound = new Error("Chat.NotFound", "Чат не найден");
|
||||||
|
public static readonly Error ChatsForbidden = new Error("Chat.Forbidden", "Доступ запрещен");
|
||||||
|
public static readonly Error MediaDisabled = new Error("Chat.MediaDisabled", "Медиафайлы отключены");
|
||||||
|
public static readonly Error PollsDisabled = new Error("Chat.PollsDisabled", "Опросы отключены");
|
||||||
|
public static readonly Error NotFound = new Error("Chat.NotFound", "Не найдено");
|
||||||
|
public static readonly Error NotMember = new Error("Chat.NotMember", "Вы не являетесь участником чата");
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@ namespace Knot.Contracts.Messaging.Application.Abstractions;
|
|||||||
public interface IMessageNotifier
|
public interface IMessageNotifier
|
||||||
{
|
{
|
||||||
Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken);
|
Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken);
|
||||||
|
Task NotifyMessageUpdateAsync(Guid chatId, string updateType, object updatePayload, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ public interface IMessageRepository
|
|||||||
Task<Message?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
Task<Message?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||||
Task<List<Message>> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken);
|
Task<List<Message>> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken);
|
||||||
Task<Message?> GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken);
|
Task<Message?> GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken);
|
||||||
|
Task<List<Message>> GetPinnedMessagesAsync(Guid chatId, CancellationToken cancellationToken);
|
||||||
Task<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken);
|
Task<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken);
|
||||||
|
|
||||||
Task<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken);
|
Task<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, long? sequenceId, int limit, CancellationToken cancellationToken);
|
||||||
|
Task<List<Message>> GetChatMessagesAroundAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken);
|
||||||
Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
|
Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
|
||||||
|
|
||||||
Task UpdateAsync(Message message, CancellationToken cancellationToken);
|
Task UpdateAsync(Message message, CancellationToken cancellationToken);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class CallMessage : Message
|
||||||
|
{
|
||||||
|
public override string Type => "call";
|
||||||
|
public override string? Content { get; protected set; }
|
||||||
|
public string CallType { get; protected set; }
|
||||||
|
public string CallStatus { get; protected set; }
|
||||||
|
public int? Duration { get; protected set; }
|
||||||
|
|
||||||
|
public CallMessage() : base() { }
|
||||||
|
|
||||||
|
public CallMessage(
|
||||||
|
Guid id,
|
||||||
|
Guid chatId,
|
||||||
|
Guid senderId,
|
||||||
|
string callType,
|
||||||
|
string callStatus,
|
||||||
|
int? duration,
|
||||||
|
Guid? replyToId,
|
||||||
|
Guid? forwardedFromId,
|
||||||
|
DateTime createdAt,
|
||||||
|
bool isImported = false)
|
||||||
|
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported)
|
||||||
|
{
|
||||||
|
CallType = callType;
|
||||||
|
CallStatus = callStatus;
|
||||||
|
Duration = duration;
|
||||||
|
Content = $"Call {callStatus}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class Media
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
public string? Url { get; set; }
|
||||||
|
public string? ThumbnailUrl { get; set; }
|
||||||
|
public long? Size { get; set; }
|
||||||
|
public int Width { get; set; }
|
||||||
|
public int Height { get; set; }
|
||||||
|
public string? FileId { get; set; }
|
||||||
|
public string? Filename { get; set; }
|
||||||
|
public string? Duration { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class MediaMessage : Message
|
||||||
|
{
|
||||||
|
public override string Type => MediaType.ToString().ToLower();
|
||||||
|
public override string? Content { get; protected set; }
|
||||||
|
public string? Caption { get => Content; private set => Content = value; }
|
||||||
|
public MediaType MediaType { get; private set; }
|
||||||
|
|
||||||
|
private List<Media> _media = new();
|
||||||
|
public IReadOnlyCollection<Media> Media => _media.AsReadOnly();
|
||||||
|
|
||||||
|
private MediaMessage() : base() { MediaType = MediaType.File; }
|
||||||
|
|
||||||
|
public MediaMessage(Guid id, Guid chatId, Guid senderId, MediaType mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||||
|
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) { MediaType = mediaType; Caption = caption; }
|
||||||
|
|
||||||
|
public MediaMessage(Guid id, Guid chatId, Guid senderId, string mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||||
|
: this(id, chatId, senderId, Enum.TryParse<MediaType>(mediaType, true, out var mt) ? mt : MediaType.File, caption, replyToId, forwardedFromId, createdAt, isImported) { }
|
||||||
|
|
||||||
|
public void AddMedia(string type, string url, string? filename, long? size, string? duration = null) => _media.Add(new Media { Type = type, Url = url, Filename = filename, FileId = filename, Size = size, Duration = duration });
|
||||||
|
|
||||||
|
public override void Edit(string newCaption) => base.Edit(newCaption);
|
||||||
|
|
||||||
|
public override void Delete() { Caption = null; base.Delete(); }
|
||||||
|
}
|
||||||
@@ -16,18 +16,17 @@ public abstract class Message : AggregateRoot<Guid>
|
|||||||
public MessageState State { get; protected set; }
|
public MessageState State { get; protected set; }
|
||||||
public abstract string Type { get; }
|
public abstract string Type { get; }
|
||||||
public abstract string? Content { get; protected set; }
|
public abstract string? Content { get; protected set; }
|
||||||
public virtual string? Quote { get; protected set; } = null;
|
|
||||||
public virtual Guid? StoryId => null;
|
|
||||||
public virtual string? StoryMediaUrl => null;
|
|
||||||
public virtual string? StoryMediaType => null;
|
|
||||||
public virtual IReadOnlyCollection<Media> Media => Array.Empty<Media>();
|
|
||||||
public bool IsEdited => HasState(MessageState.IsEdited);
|
public bool IsEdited => HasState(MessageState.IsEdited);
|
||||||
public bool IsDeleted => HasState(MessageState.IsDeleted);
|
public bool IsDeleted => HasState(MessageState.IsDeleted);
|
||||||
public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId);
|
|
||||||
protected List<DeletedMessage> _deletedFor = new();
|
protected List<DeletedMessage> _deletedFor = new();
|
||||||
public IReadOnlyCollection<DeletedMessage> DeletedFor => _deletedFor.AsReadOnly();
|
public IReadOnlyCollection<DeletedMessage> DeletedFor => _deletedFor.AsReadOnly();
|
||||||
|
|
||||||
protected Message() : base(Guid.Empty) { }
|
protected Message() : base(Guid.Empty) { }
|
||||||
protected Message(Guid id, Guid chatId, Guid senderId, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported) : base(id)
|
|
||||||
|
protected Message(Guid id, Guid chatId, Guid senderId, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||||
|
: base(id)
|
||||||
{
|
{
|
||||||
ChatId = chatId;
|
ChatId = chatId;
|
||||||
SenderId = senderId;
|
SenderId = senderId;
|
||||||
@@ -36,91 +35,22 @@ public abstract class Message : AggregateRoot<Guid>
|
|||||||
CreatedAt = createdAt;
|
CreatedAt = createdAt;
|
||||||
if (isImported) AddState(MessageState.IsImported);
|
if (isImported) AddState(MessageState.IsImported);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddState(MessageState state) => State |= state;
|
public void AddState(MessageState state) => State |= state;
|
||||||
public void RemoveState(MessageState state) => State &= ~state;
|
public void RemoveState(MessageState state) => State &= ~state;
|
||||||
public bool HasState(MessageState state) => (State & state) == state;
|
public bool HasState(MessageState state) => (State & state) == state;
|
||||||
|
public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId);
|
||||||
|
|
||||||
public virtual void Delete() => AddState(MessageState.IsDeleted);
|
public virtual void Delete() => AddState(MessageState.IsDeleted);
|
||||||
public virtual void Edit(string newContent) { Content = newContent; AddState(MessageState.IsEdited); }
|
public virtual void Edit(string newContent)
|
||||||
public void DeleteForUser(Guid userId) { if (!_deletedFor.Exists(x => x.UserId == userId)) _deletedFor.Add(new DeletedMessage(Id, userId)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Media
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public string Type { get; set; } = string.Empty;
|
|
||||||
public string? Url { get; set; }
|
|
||||||
public string? ThumbnailUrl { get; set; }
|
|
||||||
public long? Size { get; set; }
|
|
||||||
public int Width { get; set; }
|
|
||||||
public int Height { get; set; }
|
|
||||||
public string? FileId { get; set; }
|
|
||||||
public string? Filename { get; set; }
|
|
||||||
public string? Duration { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TextMessage : Message
|
|
||||||
{
|
|
||||||
public override string Type => "text";
|
|
||||||
public override string? Content { get; protected set; }
|
|
||||||
public TextMessage() : base() { }
|
|
||||||
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
|
|
||||||
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) => Content = content;
|
|
||||||
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, string? quote, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
|
|
||||||
: this(id, chatId, senderId, content, replyToId, forwardedFromId, createdAt, isImported) => Quote = quote;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MediaMessage : Message
|
|
||||||
{
|
|
||||||
public override string Type => MediaType.ToString().ToLower();
|
|
||||||
public override string? Content { get; protected set; }
|
|
||||||
public string? Caption { get => Content; private set => Content = value; }
|
|
||||||
public MediaType MediaType { get; private set; }
|
|
||||||
private List<Media> _media = new();
|
|
||||||
public override IReadOnlyCollection<Media> Media => _media.AsReadOnly();
|
|
||||||
private MediaMessage() : base() { MediaType = MediaType.File; }
|
|
||||||
public MediaMessage(Guid id, Guid chatId, Guid senderId, MediaType mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
|
||||||
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) { MediaType = mediaType; Caption = caption; }
|
|
||||||
public MediaMessage(Guid id, Guid chatId, Guid senderId, string mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
|
||||||
: this(id, chatId, senderId, Enum.TryParse<MediaType>(mediaType, true, out var mt) ? mt : MediaType.File, caption, replyToId, forwardedFromId, createdAt, isImported) { }
|
|
||||||
public void AddMedia(string type, string url, string? filename, long? size) => _media.Add(new Media { Type = type, Url = url, FileId = filename, Size = size });
|
|
||||||
public override void Edit(string newCaption) => base.Edit(newCaption);
|
|
||||||
public override void Delete() { Caption = null; base.Delete(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StoryMessage : Message
|
|
||||||
{
|
|
||||||
public override string Type => "story";
|
|
||||||
public override string? Content { get; protected set; }
|
|
||||||
public override Guid? StoryId { get; }
|
|
||||||
public string? InternalStoryMediaUrl { get; private set; }
|
|
||||||
public override string? StoryMediaUrl => InternalStoryMediaUrl;
|
|
||||||
public override string? StoryMediaType { get; }
|
|
||||||
public StoryMessage() : base() { }
|
|
||||||
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, DateTime createdAt)
|
|
||||||
: base(id, chatId, senderId, null, null, createdAt, false) { StoryId = storyId; InternalStoryMediaUrl = storyMediaUrl; StoryMediaType = storyMediaType; }
|
|
||||||
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, string? content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
|
||||||
: this(id, chatId, senderId, storyId, storyMediaUrl, storyMediaType, createdAt) { Content = content; ReplyToId = replyToId; ForwardedFromId = forwardedFromId; if (isImported) AddState(MessageState.IsImported); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PollMessage : Message
|
|
||||||
{
|
|
||||||
public override string Type => "poll";
|
|
||||||
public override string? Content { get; protected set; }
|
|
||||||
public List<PollOption> Options { get; } = new();
|
|
||||||
public List<PollVote> Votes { get; } = new();
|
|
||||||
public bool IsMultipleChoice { get; set; }
|
|
||||||
public DateTime? ExpiresAt { get; set; }
|
|
||||||
public bool IsClosed { get; set; }
|
|
||||||
public PollMessage() : base() { }
|
|
||||||
public PollMessage(Guid id, Guid chatId, Guid senderId, string? question, List<string>? options, bool isAnonymous, bool isMultiple, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
|
||||||
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported)
|
|
||||||
{
|
{
|
||||||
Content = question ?? "Poll";
|
Content = newContent;
|
||||||
if (options != null) foreach (var opt in options) Options.Add(new PollOption { Text = opt });
|
AddState(MessageState.IsEdited);
|
||||||
IsMultipleChoice = isMultiple;
|
}
|
||||||
ExpiresAt = expiresAt;
|
|
||||||
|
public void DeleteForUser(Guid userId)
|
||||||
|
{
|
||||||
|
if (!_deletedFor.Exists(x => x.UserId == userId))
|
||||||
|
_deletedFor.Add(new DeletedMessage(Id, userId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PollOption { public string Text { get; set; } = string.Empty; public int VoteCount { get; set; } }
|
|
||||||
public class PollVote { public Guid OptionIndex { get; set; } public Guid UserId { get; set; } public DateTime VotedAt { get; set; } }
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class PollMessage : Message
|
||||||
|
{
|
||||||
|
public override string Type => "poll";
|
||||||
|
public override string? Content { get; protected set; }
|
||||||
|
public List<PollOption> Options { get; set; } = new();
|
||||||
|
public List<PollVote> Votes { get; set; } = new();
|
||||||
|
public bool IsMultipleChoice { get; set; }
|
||||||
|
public bool IsAnonymous { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
public bool IsClosed { get; set; }
|
||||||
|
|
||||||
|
public PollMessage() : base() { }
|
||||||
|
|
||||||
|
public PollMessage(Guid id, Guid chatId, Guid senderId, string? question, List<PollOption>? options, bool isAnonymous, bool isMultiple, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||||
|
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported)
|
||||||
|
{
|
||||||
|
Content = question ?? "Poll";
|
||||||
|
Options = options ?? new List<PollOption>();
|
||||||
|
IsAnonymous = isAnonymous;
|
||||||
|
IsMultipleChoice = isMultiple;
|
||||||
|
ExpiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PollMessage Create(Guid id, Guid chatId, Guid senderId, string? question, List<string> options, bool isAnonymous, bool isMultiple, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId)
|
||||||
|
{
|
||||||
|
var poll = new PollMessage(id, chatId, senderId, question, null, isAnonymous, isMultiple, expiresAt, replyToId, forwardedFromId, DateTime.UtcNow, false);
|
||||||
|
foreach (var opt in options)
|
||||||
|
{
|
||||||
|
poll.Options.Add(new PollOption { Text = opt });
|
||||||
|
}
|
||||||
|
return poll;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class PollOption
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
public int VoteCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class PollVote
|
||||||
|
{
|
||||||
|
public Guid OptionId { get; set; }
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public DateTime VotedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class StoryMessage : Message
|
||||||
|
{
|
||||||
|
public override string Type => "story";
|
||||||
|
public override string? Content { get; protected set; }
|
||||||
|
public Guid? StoryId { get; private set; }
|
||||||
|
public string? StoryMediaUrl { get; private set; }
|
||||||
|
public string? StoryMediaType { get; private set; }
|
||||||
|
|
||||||
|
public StoryMessage() : base() { }
|
||||||
|
|
||||||
|
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, DateTime createdAt)
|
||||||
|
: base(id, chatId, senderId, null, null, createdAt, false)
|
||||||
|
{
|
||||||
|
StoryId = storyId;
|
||||||
|
StoryMediaUrl = storyMediaUrl;
|
||||||
|
StoryMediaType = storyMediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, string? content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||||
|
: this(id, chatId, senderId, storyId, storyMediaUrl, storyMediaType, createdAt)
|
||||||
|
{
|
||||||
|
Content = content;
|
||||||
|
ReplyToId = replyToId;
|
||||||
|
ForwardedFromId = forwardedFromId;
|
||||||
|
if (isImported) AddState(MessageState.IsImported);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Messaging.Domain;
|
||||||
|
|
||||||
|
public class TextMessage : Message
|
||||||
|
{
|
||||||
|
public override string Type => "text";
|
||||||
|
public override string? Content { get; protected set; }
|
||||||
|
public string? Quote { get; protected set; }
|
||||||
|
|
||||||
|
public TextMessage() : base() { }
|
||||||
|
|
||||||
|
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
|
||||||
|
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) => Content = content;
|
||||||
|
|
||||||
|
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, string? quote, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
|
||||||
|
: this(id, chatId, senderId, content, replyToId, forwardedFromId, createdAt, isImported) => Quote = quote;
|
||||||
|
}
|
||||||
@@ -9,4 +9,8 @@ public static class ProfilesErrors
|
|||||||
public static Error AvatarNotFound => new("Profiles.AvatarNotFound", "Avatar not found");
|
public static Error AvatarNotFound => new("Profiles.AvatarNotFound", "Avatar not found");
|
||||||
public static Error InvalidAvatarFormat => new("Profiles.InvalidAvatarFormat", "Invalid avatar format");
|
public static Error InvalidAvatarFormat => new("Profiles.InvalidAvatarFormat", "Invalid avatar format");
|
||||||
public static Error AvatarUploadFailed => new("Profiles.AvatarUploadFailed", "Avatar upload failed");
|
public static Error AvatarUploadFailed => new("Profiles.AvatarUploadFailed", "Avatar upload failed");
|
||||||
|
public static Error BioTooLong => new("Profiles.BioTooLong", "Bio must be 200 characters or less");
|
||||||
|
public static Error StatusTextTooLong => new("Profiles.StatusTextTooLong", "Status text must be 50 characters or less");
|
||||||
|
public static Error StatusEmpty => new("Statuses.Empty", "Status emoji or text is required");
|
||||||
|
public static Error InvalidPreset => new("Statuses.InvalidPreset", "Unknown status preset");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Knot.Contracts.Profiles.Application.DTOs;
|
||||||
|
|
||||||
|
public sealed class StatusPresetDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Emoji { get; set; } = "";
|
||||||
|
public string TextRu { get; set; } = "";
|
||||||
|
public string TextEn { get; set; } = "";
|
||||||
|
}
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
namespace Knot.Contracts.Profiles.Application.DTOs;
|
namespace Knot.Contracts.Profiles.Application.DTOs;
|
||||||
|
|
||||||
public class UserProfileDto
|
public class UserProfileDto
|
||||||
{
|
{
|
||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
|
public Guid Id { get => UserId; set => UserId = value; }
|
||||||
public string? DisplayName { get; set; }
|
public string? DisplayName { get; set; }
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
public string? About { get; set; }
|
public string? About { get; set; }
|
||||||
public string? Avatar { get; set; }
|
public string? Avatar { get; set; }
|
||||||
public bool IsBot { get; set; }
|
public bool IsBot { get; set; }
|
||||||
public DateTime? LastSeen { get; set; }
|
public DateTime? LastSeen { get; set; }
|
||||||
|
public DateTime? Birthday { get; set; }
|
||||||
public bool IsPremium { get; set; }
|
public bool IsPremium { get; set; }
|
||||||
|
public UserStatusDto? Status { get; set; }
|
||||||
|
|
||||||
|
public Guid? CurrentStatusId { get; set; }
|
||||||
|
|
||||||
|
public string? StatusText { get; set; }
|
||||||
|
public string? StatusEmoji { get; set; }
|
||||||
|
public DateTime? StatusExpiresAt { get; set; }
|
||||||
|
public bool IsInvisible { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Knot.Contracts.Profiles.Application.DTOs;
|
||||||
|
|
||||||
|
public sealed class UserStatusDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Type { get; set; } = "Custom";
|
||||||
|
public string Emoji { get; set; } = "";
|
||||||
|
public string Text { get; set; } = "";
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Knot.Contracts.Profiles.Application.DTOs;
|
using Knot.Contracts.Profiles.Application.DTOs;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
namespace Knot.Contracts.Profiles.Domain;
|
namespace Knot.Contracts.Profiles.Domain;
|
||||||
@@ -12,4 +12,5 @@ public interface IProfileRepository
|
|||||||
Task<Result<UserProfileDto>> CreateAsync(UserProfileDto dto, CancellationToken cancellationToken = default);
|
Task<Result<UserProfileDto>> CreateAsync(UserProfileDto dto, CancellationToken cancellationToken = default);
|
||||||
Task<Result<UserProfileDto>> UpdateAsync(UserProfileDto dto, CancellationToken cancellationToken = default);
|
Task<Result<UserProfileDto>> UpdateAsync(UserProfileDto dto, CancellationToken cancellationToken = default);
|
||||||
Task<Result> DeleteAsync(Guid userId, CancellationToken cancellationToken = default);
|
Task<Result> DeleteAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
Task<Result> UpdateStatusAsync(Guid userId, bool isBanned, bool isDeleted, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Knot.Contracts.Profiles.Application.DTOs;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Profiles.Domain;
|
||||||
|
|
||||||
|
public interface IProfileStatusWriter
|
||||||
|
{
|
||||||
|
Task<Result<UserProfileDto>> SetCustomAsync(Guid userId, string emoji, string text, DateTime? expiresAt, string? presetKey, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<Result<UserProfileDto>> ClearAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Knot.Contracts.Profiles.Application.DTOs;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Profiles.Domain;
|
||||||
|
|
||||||
|
public interface IUserStatusRepository
|
||||||
|
{
|
||||||
|
Task<UserStatusDto?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<IReadOnlyDictionary<Guid, UserStatusDto>> GetByIdsAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task InsertAsync(UserStatusDto dto, Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Relations.Application.Contacts;
|
||||||
|
|
||||||
|
public record CheckBlockedStatusQuery(Guid UserId, List<Guid> CandidateIds) : IQuery<List<Guid>>;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Relations.Application.Contacts;
|
||||||
|
|
||||||
|
public record GetBlockedUserIdsQuery(Guid UserId) : IQuery<List<Guid>>;
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Knot.Contracts.Settings.Abstractions;
|
|
||||||
|
|
||||||
public interface IStatisticsService
|
|
||||||
{
|
|
||||||
Task<DashboardStatsDto> GetDashboardStatsAsync(CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DashboardStatsDto
|
|
||||||
{
|
|
||||||
public long StorageUsedBytes { get; set; }
|
|
||||||
public long StorageLimitBytes { get; set; }
|
|
||||||
public long OnlineUsers { get; set; }
|
|
||||||
public long OfflineUsers { get; set; }
|
|
||||||
public long TotalUsers { get; set; }
|
|
||||||
public List<ActivityStatDto> ActivityTimeline { get; set; } = new();
|
|
||||||
public List<TopUserDto> TopUsersByMessages { get; set; } = new();
|
|
||||||
public List<TopUserDto> TopUsersByStorage { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ActivityStatDto
|
|
||||||
{
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public long Messages { get; set; }
|
|
||||||
public long FilesSize { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TopUserDto
|
|
||||||
{
|
|
||||||
public Guid UserId { get; set; }
|
|
||||||
public string Username { get; set; } = string.Empty;
|
|
||||||
public long Value { get; set; } // messages count or bytes
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Knot.Contracts.Settings.Application.DTOs;
|
namespace Knot.Contracts.Settings.Application.DTOs;
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ public class MessagesConfig
|
|||||||
public class WebRtcConfig
|
public class WebRtcConfig
|
||||||
{
|
{
|
||||||
public bool Enabled { get; set; } = false;
|
public bool Enabled { get; set; } = false;
|
||||||
|
public bool EnableVoiceCalls { get; set; } = true;
|
||||||
public bool EnableVideoCalls { get; set; } = true;
|
public bool EnableVideoCalls { get; set; } = true;
|
||||||
public bool EnableScreenSharing { get; set; } = true;
|
public bool EnableScreenSharing { get; set; } = true;
|
||||||
public string TurnHost { get; set; } = string.Empty;
|
public string TurnHost { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
||||||
<PackageReference Include="Carter" Version="10.0.0" />
|
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
|
|||||||
+68
-27
@@ -1,31 +1,47 @@
|
|||||||
using Knot.Modules.Messaging;
|
|
||||||
using Carter;
|
|
||||||
using Knot.Shared.Infrastructure;
|
|
||||||
using Knot.Modules.Auth;
|
|
||||||
using Knot.Modules.Profiles;
|
|
||||||
using Knot.Modules.Settings;
|
|
||||||
using Knot.Modules.Admin;
|
|
||||||
using Knot.Modules.Conversations;
|
|
||||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
|
||||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
|
||||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
|
||||||
using Knot.Modules.Storage;
|
|
||||||
using Knot.Modules.Stories;
|
|
||||||
using Knot.Modules.Klipy;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using System.Text;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Knot.Modules.Admin;
|
||||||
|
using Knot.Modules.Admin.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Auth;
|
||||||
|
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||||
|
using Knot.Modules.Auth.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Conversations;
|
||||||
|
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||||
|
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||||
|
using Knot.Modules.Conversations.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Federation;
|
||||||
|
using Knot.Modules.Federation.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Klipy;
|
||||||
|
using Knot.Modules.Klipy.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Messaging;
|
||||||
|
using Knot.Modules.Profiles;
|
||||||
|
using Knot.Modules.Profiles.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Relations;
|
||||||
|
using Knot.Modules.Relations.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Settings;
|
||||||
|
using Knot.Modules.Settings.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Storage;
|
||||||
|
using Knot.Modules.Storage.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.Stories;
|
||||||
|
using Knot.Modules.Stories.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.TelegramImport;
|
||||||
|
using Knot.Modules.TelegramImport.Presentation.Endpoints;
|
||||||
|
using Knot.Modules.WebRtc;
|
||||||
|
using Knot.Modules.WebRtc.Presentation.Endpoints;
|
||||||
|
using Knot.Shared.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using System.Security.Claims;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -33,7 +49,7 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
// Маппинг стандартных переменных окружения в иерархию .NET
|
// Маппинг стандартных переменных окружения в иерархию .NET
|
||||||
var envMappings = new Dictionary<string, string?>
|
var envMappings = new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"] ?? "Host=localhost;Database=knot;Username=postgres;Password=postgres",
|
["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"] ?? builder.Configuration.GetConnectionString("DefaultConnection") ?? "Host=localhost;Database=knot;Username=postgres;Password=postgres",
|
||||||
["ConnectionStrings:MongoConnection"] = builder.Configuration["MONGO_CONNECTION"],
|
["ConnectionStrings:MongoConnection"] = builder.Configuration["MONGO_CONNECTION"],
|
||||||
["Jwt:Secret"] = builder.Configuration["JWT_SECRET"],
|
["Jwt:Secret"] = builder.Configuration["JWT_SECRET"],
|
||||||
["Jwt:Issuer"] = builder.Configuration["JWT_ISSUER"],
|
["Jwt:Issuer"] = builder.Configuration["JWT_ISSUER"],
|
||||||
@@ -60,10 +76,13 @@ builder.Services.AddSettingsModule(builder.Configuration);
|
|||||||
builder.Services.AddMessagingModule(builder.Configuration);
|
builder.Services.AddMessagingModule(builder.Configuration);
|
||||||
builder.Services.AddConversationsModule(builder.Configuration);
|
builder.Services.AddConversationsModule(builder.Configuration);
|
||||||
builder.Services.AddProfilesModule(builder.Configuration);
|
builder.Services.AddProfilesModule(builder.Configuration);
|
||||||
|
builder.Services.AddRelationsModule(builder.Configuration);
|
||||||
builder.Services.AddStorageModule(builder.Configuration);
|
builder.Services.AddStorageModule(builder.Configuration);
|
||||||
builder.Services.AddStoriesModule(builder.Configuration);
|
builder.Services.AddStoriesModule(builder.Configuration);
|
||||||
builder.Services.AddKlipyModule();
|
builder.Services.AddKlipyModule();
|
||||||
builder.Services.AddAdminModule();
|
builder.Services.AddAdminModule();
|
||||||
|
builder.Services.AddWebRtcModule();
|
||||||
|
builder.Services.AddTelegramImportModule();
|
||||||
builder.Services.AddSharedInfrastructure(builder.Configuration);
|
builder.Services.AddSharedInfrastructure(builder.Configuration);
|
||||||
|
|
||||||
// CQRS / MediatR для команд в Host (например, AdminController)
|
// CQRS / MediatR для команд в Host (например, AdminController)
|
||||||
@@ -74,12 +93,14 @@ builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
|
|||||||
typeof(Knot.Modules.Messaging.DependencyInjection).Assembly,
|
typeof(Knot.Modules.Messaging.DependencyInjection).Assembly,
|
||||||
typeof(Knot.Modules.Conversations.DependencyInjection).Assembly,
|
typeof(Knot.Modules.Conversations.DependencyInjection).Assembly,
|
||||||
typeof(Knot.Modules.Stories.DependencyInjection).Assembly,
|
typeof(Knot.Modules.Stories.DependencyInjection).Assembly,
|
||||||
typeof(Knot.Modules.Klipy.DependencyInjection).Assembly
|
typeof(Knot.Modules.Klipy.DependencyInjection).Assembly,
|
||||||
|
typeof(Knot.Modules.Relations.DependencyInjection).Assembly,
|
||||||
|
typeof(Knot.Modules.WebRtc.DependencyInjection).Assembly,
|
||||||
|
typeof(Knot.Modules.TelegramImport.DependencyInjection).Assembly,
|
||||||
|
typeof(Knot.Modules.Auth.Infrastructure.Persistence.AuthDbContext).Assembly,
|
||||||
|
typeof(Knot.Modules.Profiles.DependencyInjection).Assembly
|
||||||
));
|
));
|
||||||
|
|
||||||
// Carter для вызова Minimal APIs (Endpoints)
|
|
||||||
builder.Services.AddCarter();
|
|
||||||
|
|
||||||
// Настройка CORS
|
// Настройка CORS
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
@@ -185,6 +206,9 @@ using (var scope = app.Services.CreateScope())
|
|||||||
var storiesDb = scope.ServiceProvider.GetRequiredService<Knot.Modules.Stories.Infrastructure.Database.StoriesDbContext>();
|
var storiesDb = scope.ServiceProvider.GetRequiredService<Knot.Modules.Stories.Infrastructure.Database.StoriesDbContext>();
|
||||||
await storiesDb.Database.MigrateAsync();
|
await storiesDb.Database.MigrateAsync();
|
||||||
|
|
||||||
|
var relationsDb = scope.ServiceProvider.GetRequiredService<Knot.Modules.Relations.Infrastructure.Persistence.RelationsDbContext>();
|
||||||
|
await relationsDb.Database.MigrateAsync();
|
||||||
|
|
||||||
// Set Encryption Service for MongoDB serializers
|
// Set Encryption Service for MongoDB serializers
|
||||||
var encryptionService = scope.ServiceProvider.GetRequiredService<Knot.Shared.Kernel.Security.IEncryptionService>();
|
var encryptionService = scope.ServiceProvider.GetRequiredService<Knot.Shared.Kernel.Security.IEncryptionService>();
|
||||||
Knot.Modules.Messaging.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService;
|
Knot.Modules.Messaging.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService;
|
||||||
@@ -195,6 +219,10 @@ using (var scope = app.Services.CreateScope())
|
|||||||
{
|
{
|
||||||
concreteSettings.Initialize();
|
concreteSettings.Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync user replicas for Relations module on startup
|
||||||
|
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||||
|
await mediator.Send(new Knot.Modules.Relations.Application.Contacts.SyncReplicasCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Настройка конвейера запросов
|
// Настройка конвейера запросов
|
||||||
@@ -216,8 +244,21 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
// Добавляем контроллеры и Carter (Minimal APIs)
|
// Регистрация эндпоинтов
|
||||||
app.MapCarter();
|
app.MapAuthEndpoints();
|
||||||
|
app.MapStoriesEndpoints();
|
||||||
|
app.MapContactsEndpoints();
|
||||||
|
app.MapSettingsEndpoints();
|
||||||
|
app.MapProfilesEndpoints();
|
||||||
|
app.MapStatusesEndpoints();
|
||||||
|
app.MapFederationEndpoints();
|
||||||
|
app.MapKlipyEndpoints();
|
||||||
|
app.MapChatsEndpoints();
|
||||||
|
app.MapMessagesEndpoints();
|
||||||
|
app.MapAdminEndpoints();
|
||||||
|
app.MapFilesEndpoints();
|
||||||
|
app.MapWebRtcEndpoints();
|
||||||
|
app.MapTelegramImportEndpoints();
|
||||||
|
|
||||||
// Добавляем SignalR хабы
|
// Добавляем SignalR хабы
|
||||||
app.MapHub<ChatHub>("/hubs/chat");
|
app.MapHub<ChatHub>("/hubs/chat");
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Knot.Contracts.Auth.Infrastructure.Persistence;
|
|||||||
using Knot.Contracts.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
using Knot.Contracts.Storage.Abstractions;
|
using Knot.Shared.Kernel.Storage;
|
||||||
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -51,7 +51,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
|||||||
var orphanMessages = await _messageQueryService.GetOrphanedMessagesAsync(activeChatIds, cancellationToken);
|
var orphanMessages = await _messageQueryService.GetOrphanedMessagesAsync(activeChatIds, cancellationToken);
|
||||||
|
|
||||||
var keptMessages = allMessages
|
var keptMessages = allMessages
|
||||||
.Where(m => !orphanMessages.Any(om => om.Id == m.Id))
|
.Where(m => !orphanMessages.Any(om => om.Id == m.Id) && !m.IsDeleted)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using BCrypt.Net;
|
||||||
|
using Knot.Contracts.Auth.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Auth.Domain;
|
||||||
|
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Admin.Application.Admin.Commands.CreateUser;
|
||||||
|
|
||||||
|
public record CreateUserCommand(
|
||||||
|
string Username,
|
||||||
|
string Password,
|
||||||
|
string DisplayName,
|
||||||
|
string? Email = null,
|
||||||
|
string? Bio = null
|
||||||
|
) : ICommand<AdminUserDto>;
|
||||||
|
|
||||||
|
internal sealed class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, AdminUserDto>
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IAuthUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public CreateUserCommandHandler(IUserRepository userRepository, IAuthUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<AdminUserDto>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Проверка уникальности username
|
||||||
|
if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken))
|
||||||
|
{
|
||||||
|
return Result.Failure<AdminUserDto>(new Error("Admin.CreateUser.UsernameNotUnique", "Username is already taken"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хеширование пароля
|
||||||
|
string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password);
|
||||||
|
|
||||||
|
// Создание контракта пользователя
|
||||||
|
var userContract = new UserContract
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = request.Username,
|
||||||
|
PasswordHash = passwordHash,
|
||||||
|
DisplayName = request.DisplayName,
|
||||||
|
Email = request.Email,
|
||||||
|
Bio = request.Bio,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
IsBanned = false,
|
||||||
|
IsOnline = false
|
||||||
|
};
|
||||||
|
|
||||||
|
_unitOfWork.Add(userContract);
|
||||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Success(new AdminUserDto(
|
||||||
|
userContract.Id,
|
||||||
|
userContract.Username,
|
||||||
|
userContract.DisplayName,
|
||||||
|
userContract.Email,
|
||||||
|
userContract.Avatar,
|
||||||
|
userContract.CreatedAt,
|
||||||
|
userContract.IsOnline,
|
||||||
|
userContract.LastSeen ?? DateTime.UtcNow,
|
||||||
|
userContract.IsBanned));
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-14
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -8,6 +8,7 @@ using Knot.Contracts.Auth.Infrastructure.Persistence;
|
|||||||
using Knot.Contracts.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
|
using Knot.Shared.Kernel.Storage;
|
||||||
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -23,17 +24,20 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
|||||||
private readonly IAuthDbContext _authDbContext;
|
private readonly IAuthDbContext _authDbContext;
|
||||||
private readonly IChatsDbContext _chatsDbContext;
|
private readonly IChatsDbContext _chatsDbContext;
|
||||||
private readonly IStoryCollection _storyCollection;
|
private readonly IStoryCollection _storyCollection;
|
||||||
|
private readonly IFileStorageService _fileStorage;
|
||||||
|
|
||||||
public CleanDryRunQueryHandler(
|
public CleanDryRunQueryHandler(
|
||||||
Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService messageService,
|
Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService messageService,
|
||||||
IAuthDbContext authDbContext,
|
IAuthDbContext authDbContext,
|
||||||
IChatsDbContext chatsDbContext,
|
IChatsDbContext chatsDbContext,
|
||||||
IStoryCollection storyCollection)
|
IStoryCollection storyCollection,
|
||||||
|
IFileStorageService fileStorage)
|
||||||
{
|
{
|
||||||
_messageService = messageService;
|
_messageService = messageService;
|
||||||
_authDbContext = authDbContext;
|
_authDbContext = authDbContext;
|
||||||
_chatsDbContext = chatsDbContext;
|
_chatsDbContext = chatsDbContext;
|
||||||
_storyCollection = storyCollection;
|
_storyCollection = storyCollection;
|
||||||
|
_fileStorage = fileStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<CleanDryRunResult>> Handle(CleanDryRunQuery request, CancellationToken ct)
|
public async Task<Result<CleanDryRunResult>> Handle(CleanDryRunQuery request, CancellationToken ct)
|
||||||
@@ -46,25 +50,42 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
|||||||
var orphanedMediaCount = orphanedMessages.Count(m => m.MediaUrl != null);
|
var orphanedMediaCount = orphanedMessages.Count(m => m.MediaUrl != null);
|
||||||
var orphanedMessageCount = orphanedMessages.Count;
|
var orphanedMessageCount = orphanedMessages.Count;
|
||||||
|
|
||||||
var validIds = new HashSet<string>();
|
var allMessages = await _messageService.GetAllMessagesAsync(ct);
|
||||||
foreach (var msg in orphanedMessages.Where(m => m.MediaUrl != null))
|
var keptMessages = allMessages
|
||||||
{
|
.Where(m => !orphanedMessages.Any(om => om.Id == m.Id) && !m.IsDeleted)
|
||||||
var parts = msg.MediaUrl.Split('/');
|
.ToList();
|
||||||
var fileId = parts.LastOrDefault();
|
var allUsers = await _authDbContext.Users.ToListAsync(ct);
|
||||||
if (!string.IsNullOrEmpty(fileId))
|
|
||||||
{
|
|
||||||
validIds.Add(fileId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var stories = await _storyCollection.GetAllAsync(ct);
|
var stories = await _storyCollection.GetAllAsync(ct);
|
||||||
|
|
||||||
|
var validUrls = new HashSet<string>();
|
||||||
|
|
||||||
|
var activeMessageUrls = keptMessages.Where(m => m.Media != null).SelectMany(m => m.Media!).Select(x => x.Url).Where(u => !string.IsNullOrEmpty(u));
|
||||||
|
var activeChatUrls = _chatsDbContext.Chats.Select(c => c.Avatar).Where(u => !string.IsNullOrEmpty(u));
|
||||||
|
var activeUserUrls = allUsers.Select(u => u.Avatar).Where(u => !string.IsNullOrEmpty(u));
|
||||||
|
var activeStoryUrls = stories.Select(s => s.MediaUrl).Where(u => !string.IsNullOrEmpty(u));
|
||||||
|
|
||||||
|
foreach (var u in activeMessageUrls) validUrls.Add(u!);
|
||||||
|
foreach (var u in activeChatUrls) validUrls.Add(u!);
|
||||||
|
foreach (var u in activeUserUrls) validUrls.Add(u!);
|
||||||
|
foreach (var u in activeStoryUrls) validUrls.Add(u!);
|
||||||
|
|
||||||
|
var validFileIds = validUrls
|
||||||
|
.Where(u => u.Contains("/api/files/"))
|
||||||
|
.Select(u => u.Split('/').Last())
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var allMinioFiles = await _fileStorage.ListFilesAsync();
|
||||||
|
long orphanedFileSize = allMinioFiles
|
||||||
|
.Where(f => !validFileIds.Contains(f.FileId))
|
||||||
|
.Sum(f => f.Size);
|
||||||
|
|
||||||
var expiredStoriesCount = stories.Count(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow);
|
var expiredStoriesCount = stories.Count(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow);
|
||||||
var expiredStoriesSize = stories.Where(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow).Sum(s => s.MediaUrl?.Length ?? 0);
|
var expiredStoriesSize = stories.Where(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow).Sum(s => s.MediaUrl?.Length ?? 0);
|
||||||
|
|
||||||
return Result.Success(new CleanDryRunResult(
|
return Result.Success(new CleanDryRunResult(
|
||||||
orphanedMessageCount,
|
orphanedMessageCount,
|
||||||
orphanedMediaCount,
|
orphanedMediaCount,
|
||||||
0,
|
orphanedFileSize,
|
||||||
expiredStoriesCount,
|
expiredStoriesCount,
|
||||||
expiredStoriesSize
|
expiredStoriesSize
|
||||||
));
|
));
|
||||||
|
|||||||
+1
-1
@@ -1,8 +1,8 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Contracts.Admin.Abstractions;
|
using Knot.Contracts.Admin.Abstractions;
|
||||||
using Knot.Contracts.Settings.Abstractions;
|
|
||||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||||
|
using Knot.Shared.Kernel.Services;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||||
|
|||||||
+3
-5
@@ -1,11 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Contracts.Admin.Abstractions;
|
using Knot.Contracts.Admin.Abstractions;
|
||||||
using Knot.Contracts.Auth.Domain;
|
using Knot.Contracts.Auth.Domain;
|
||||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Abstractions;
|
||||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -18,12 +16,12 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
|
|||||||
{
|
{
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
|
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
|
||||||
private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService;
|
private readonly IUserStatusService _statusService;
|
||||||
|
|
||||||
public GetUserDetailsQueryHandler(
|
public GetUserDetailsQueryHandler(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
|
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
|
||||||
Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService)
|
IUserStatusService statusService)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_statsService = statsService;
|
_statsService = statsService;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Contracts.Admin.Abstractions;
|
using Knot.Contracts.Admin.Abstractions;
|
||||||
using Knot.Contracts.Auth.Domain;
|
using Knot.Contracts.Auth.Domain;
|
||||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Abstractions;
|
||||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -18,12 +18,12 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery,
|
|||||||
{
|
{
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
|
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
|
||||||
private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService;
|
private readonly IUserStatusService _statusService;
|
||||||
|
|
||||||
public SearchUsersQueryHandler(
|
public SearchUsersQueryHandler(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
|
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
|
||||||
Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService)
|
IUserStatusService statusService)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_statsService = statsService;
|
_statsService = statsService;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
@@ -18,8 +18,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Carter" Version="10.0.0" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using Carter;
|
using System.Text.Json.Serialization;
|
||||||
|
using Knot.Contracts.Conversations.Abstractions;
|
||||||
using Knot.Contracts.Settings.Application.Abstractions;
|
using Knot.Contracts.Settings.Application.Abstractions;
|
||||||
using Knot.Contracts.Settings.Application.DTOs;
|
using Knot.Contracts.Settings.Application.DTOs;
|
||||||
using Knot.Modules.Admin.Application.Admin.Commands;
|
using Knot.Modules.Admin.Application.Admin.Commands;
|
||||||
|
using Knot.Modules.Admin.Application.Admin.Commands.CreateUser;
|
||||||
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||||
using Knot.Modules.Admin.Application.Admin.Queries;
|
using Knot.Modules.Admin.Application.Admin.Queries;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -10,14 +12,16 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
namespace Knot.Host.Presentation.Endpoints;
|
namespace Knot.Modules.Admin.Presentation.Endpoints;
|
||||||
|
|
||||||
public record KlipyTestDto(string ApiKey, string AppName);
|
public record KlipyTestDto(
|
||||||
|
[property: JsonPropertyName("apiKey")] string ApiKey,
|
||||||
|
[property: JsonPropertyName("appName")] string AppName);
|
||||||
public record ResetPasswordRequest(string NewPassword);
|
public record ResetPasswordRequest(string NewPassword);
|
||||||
|
|
||||||
public sealed class AdminEndpoints : ICarterModule
|
public static class AdminEndpoints
|
||||||
{
|
{
|
||||||
public void AddRoutes(IEndpointRouteBuilder app)
|
public static void MapAdminEndpoints(this WebApplication app)
|
||||||
{
|
{
|
||||||
var group = app.MapGroup("api/admin"); // Middleware handles auth
|
var group = app.MapGroup("api/admin"); // Middleware handles auth
|
||||||
|
|
||||||
@@ -35,7 +39,16 @@ public sealed class AdminEndpoints : ICarterModule
|
|||||||
|
|
||||||
group.MapPost("settings/test-klipy", async ([FromBody] KlipyTestDto dto, ISender sender, CancellationToken ct) =>
|
group.MapPost("settings/test-klipy", async ([FromBody] KlipyTestDto dto, ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"[Admin] TestKlipy received: ApiKey={(string.IsNullOrEmpty(dto.ApiKey) ? "EMPTY" : "present")}, AppName={(string.IsNullOrEmpty(dto.AppName) ? "EMPTY" : dto.AppName)}");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.AppName))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Admin] TestKlipy: Missing required fields - ApiKey={dto?.ApiKey}, AppName={dto?.AppName}");
|
||||||
|
return Results.BadRequest(new { error = "ApiKey and AppName are required" });
|
||||||
|
}
|
||||||
|
|
||||||
var result = await sender.Send(new TestKlipyConnectionCommand(dto.ApiKey, dto.AppName), ct);
|
var result = await sender.Send(new TestKlipyConnectionCommand(dto.ApiKey, dto.AppName), ct);
|
||||||
|
Console.WriteLine($"[Admin] TestKlipy result: IsSuccess={result.IsSuccess}, Error={result.Error?.Description}");
|
||||||
return result.IsSuccess ? Results.Ok(new { success = true }) : Results.BadRequest(new { error = result.Error.Description });
|
return result.IsSuccess ? Results.Ok(new { success = true }) : Results.BadRequest(new { error = result.Error.Description });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,7 +58,13 @@ public sealed class AdminEndpoints : ICarterModule
|
|||||||
return Results.Ok(result.Value);
|
return Results.Ok(result.Value);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordRequest dto, ISender sender, CancellationToken ct) =>
|
group.MapPost("users", async ([FromBody] CreateUserCommand command, ISender sender, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var result = await sender.Send(command, ct);
|
||||||
|
return result.IsSuccess ? Results.Created($"/api/admin/users/{result.Value.Id}", result.Value) : Results.BadRequest(new { error = result.Error.Description });
|
||||||
|
});
|
||||||
|
|
||||||
|
group.MapPost("users/{userId:guid}/reset-password", async ([FromRoute] Guid userId, [FromBody] ResetPasswordRequest dto, ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct);
|
var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct);
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
@@ -56,34 +75,51 @@ public sealed class AdminEndpoints : ICarterModule
|
|||||||
return Results.Ok(result.Value);
|
return Results.Ok(result.Value);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("users/{userId:guid}/ban", async (Guid userId, ISender sender, CancellationToken ct) =>
|
group.MapPost("users/{userId:guid}/ban", async ([FromRoute] Guid userId, ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new BanUserCommand(userId), ct);
|
var result = await sender.Send(new BanUserCommand(userId), ct);
|
||||||
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("users/{userId:guid}/unban", async (Guid userId, ISender sender, CancellationToken ct) =>
|
group.MapPost("users/{userId:guid}/unban", async ([FromRoute] Guid userId, ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new UnbanUserCommand(userId), ct);
|
var result = await sender.Send(new UnbanUserCommand(userId), ct);
|
||||||
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) =>
|
group.MapGet("users", async (ISender sender, [FromQuery] string? query, CancellationToken ct = default) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new SearchUsersQuery(query), ct);
|
// При пустом или отсутствующем query возвращаем всех пользователей
|
||||||
|
var searchQuery = string.IsNullOrWhiteSpace(query) ? "" : query;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await sender.Send(new SearchUsersQuery(searchQuery), ct);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"[Admin] SearchUsers error: {result.Error.Code} - {result.Error.Description}");
|
||||||
return Results.BadRequest(new { error = result.Error.Description });
|
return Results.BadRequest(new { error = result.Error.Description });
|
||||||
}
|
}
|
||||||
return Results.Ok(result.Value);
|
return Results.Ok(result.Value);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Admin] SearchUsers exception: {ex}");
|
||||||
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapGet("users/{id:guid}", async (Guid id, ISender sender, CancellationToken ct) =>
|
group.MapGet("users/{id:guid}", async ([FromRoute] Guid id, ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new GetUserDetailsQuery(id), ct);
|
var result = await sender.Send(new GetUserDetailsQuery(id), ct);
|
||||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found");
|
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.MapDelete("users/{userId:guid}", async ([FromRoute] Guid userId, IUserDeleterService userDeleter, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var result = await userDeleter.DeleteUserAsync(userId, ct);
|
||||||
|
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||||
|
});
|
||||||
|
|
||||||
group.MapGet("clean/dry-run", async (ISender sender, CancellationToken ct) =>
|
group.MapGet("clean/dry-run", async (ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new CleanDryRunQuery(), ct);
|
var result = await sender.Send(new CleanDryRunQuery(), ct);
|
||||||
@@ -95,6 +131,17 @@ public sealed class AdminEndpoints : ICarterModule
|
|||||||
return Results.Ok(result.Value);
|
return Results.Ok(result.Value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.MapPost("clean/run", async (ISender sender, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var result = await sender.Send(new CleanRunCommand(), ct);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Admin] Cleanup Run Error: {result.Error.Description}");
|
||||||
|
return Results.BadRequest(new { error = result.Error.Description });
|
||||||
|
}
|
||||||
|
return Results.Ok(result.Value);
|
||||||
|
});
|
||||||
|
|
||||||
group.MapGet("timezones", () =>
|
group.MapGet("timezones", () =>
|
||||||
{
|
{
|
||||||
// Получаем все системные часовые пояса и формируем удобный для фронтенда формат
|
// Получаем все системные часовые пояса и формируем удобный для фронтенда формат
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Knot.Modules.Auth.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Application.Abstractions;
|
||||||
|
|
||||||
|
public interface IAuthDbContext
|
||||||
|
{
|
||||||
|
DbSet<User> Users { get; }
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Application.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unit of Work специфичный для модуля Identity.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Application.Auth.DTOs;
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using BCrypt.Net;
|
||||||
|
using Knot.Contracts.Auth.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||||
|
using Knot.Modules.Auth.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Application.Users.ChangePassword;
|
||||||
|
|
||||||
|
public sealed record ChangePasswordCommand(
|
||||||
|
Guid UserId,
|
||||||
|
string OldPassword,
|
||||||
|
string NewPassword,
|
||||||
|
string ConfirmPassword) : ICommand;
|
||||||
|
|
||||||
|
internal sealed class ChangePasswordCommandHandler : ICommandHandler<ChangePasswordCommand>
|
||||||
|
{
|
||||||
|
private readonly IAuthDbContext _dbContext;
|
||||||
|
|
||||||
|
public ChangePasswordCommandHandler(IAuthDbContext dbContext)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> Handle(ChangePasswordCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.NewPassword != request.ConfirmPassword)
|
||||||
|
return Result.Failure(AuthErrors.PasswordConfirmationMismatch);
|
||||||
|
|
||||||
|
if (request.NewPassword.Length < 8)
|
||||||
|
return Result.Failure(AuthErrors.PasswordTooShort);
|
||||||
|
|
||||||
|
var user = await _dbContext.Set<User>()
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
return Result.Failure(AuthErrors.UserNotFound);
|
||||||
|
|
||||||
|
if (!BCrypt.Net.BCrypt.Verify(request.OldPassword, user.PasswordHash))
|
||||||
|
return Result.Failure(AuthErrors.OldPasswordInvalid);
|
||||||
|
|
||||||
|
user.ChangePassword(BCrypt.Net.BCrypt.HashPassword(request.NewPassword));
|
||||||
|
user.SetRefreshToken(null);
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Knot.Shared.Kernel;
|
|
||||||
using Knot.Contracts.Auth.Domain;
|
|
||||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||||
|
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
namespace Knot.Modules.Auth.Application.Users.GetMe;
|
namespace Knot.Modules.Auth.Application.Users.GetMe;
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ internal sealed class GetMeQueryHandler : IQueryHandler<GetMeQuery, AuthResponse
|
|||||||
RefreshToken = string.Empty,
|
RefreshToken = string.Empty,
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
DisplayName = user.DisplayName
|
DisplayName = user.DisplayName,
|
||||||
|
Avatar = user.Avatar
|
||||||
};
|
};
|
||||||
|
|
||||||
return Result.Success(response);
|
return Result.Success(response);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Contracts.Auth.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Auth.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Application.Users;
|
||||||
|
|
||||||
|
internal sealed class GetUsersExistenceQueryHandler : IQueryHandler<GetUsersExistenceQuery, List<Guid>>
|
||||||
|
{
|
||||||
|
private readonly IAuthDbContext _context;
|
||||||
|
|
||||||
|
public GetUsersExistenceQueryHandler(IAuthDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<Guid>>> Handle(GetUsersExistenceQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var existingIds = await _context.Set<Knot.Modules.Auth.Domain.User>()
|
||||||
|
.Where(u => request.UserIds.Contains(u.Id))
|
||||||
|
.Select(u => u.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Success(existingIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using BCrypt.Net;
|
using BCrypt.Net;
|
||||||
using Knot.Contracts.Auth.Application.Abstractions;
|
using Knot.Contracts.Auth.Application.Abstractions;
|
||||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||||
using Knot.Contracts.Auth.Domain;
|
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
namespace Knot.Modules.Auth.Application.Users.Login;
|
namespace Knot.Modules.Auth.Application.Users.Login;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
using Knot.Contracts.Auth.Domain;
|
using BCrypt.Net;
|
||||||
|
using BCrypt.Net;
|
||||||
using Knot.Contracts.Auth.Application.Abstractions;
|
using Knot.Contracts.Auth.Application.Abstractions;
|
||||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||||
using Knot.Modules.Auth.Domain;
|
|
||||||
using Knot.Shared.Kernel;
|
|
||||||
using Knot.Contracts.Settings.Application.Abstractions;
|
using Knot.Contracts.Settings.Application.Abstractions;
|
||||||
using Knot.Contracts.Settings.Application.DTOs;
|
using Knot.Contracts.Settings.Application.DTOs;
|
||||||
using BCrypt.Net;
|
using Knot.Modules.Auth.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
namespace Knot.Modules.Auth.Application.Users.Register;
|
namespace Knot.Modules.Auth.Application.Users.Register;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Êîìàíäà äëÿ ðåãèñòðàöèè íîâîãî ïîëüçîâàòåëÿ.
|
/// ������� ��� ����������� ������ ������������.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record RegisterUserCommand(
|
public sealed record RegisterUserCommand(
|
||||||
string Username,
|
string Username,
|
||||||
@@ -20,7 +20,7 @@ public sealed record RegisterUserCommand(
|
|||||||
string? Bio) : ICommand<AuthResponseDto>;
|
string? Bio) : ICommand<AuthResponseDto>;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Îáðàáîò÷èê êîìàíäû ðåãèñòðàöèè.
|
/// ���������� ������� �����������.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCommand, AuthResponseDto>
|
internal sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCommand, AuthResponseDto>
|
||||||
{
|
{
|
||||||
@@ -48,16 +48,16 @@ internal sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserC
|
|||||||
return Result.Failure<AuthResponseDto>(AuthErrors.IdentityRegistrationDisabled);
|
return Result.Failure<AuthResponseDto>(AuthErrors.IdentityRegistrationDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Ïðîâåðêà óíèêàëüíîñòè username
|
// 1. �������� ������������ username
|
||||||
if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken))
|
if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken))
|
||||||
{
|
{
|
||||||
return Result.Failure<AuthResponseDto>(AuthErrors.IdentityUsernameNotUnique);
|
return Result.Failure<AuthResponseDto>(AuthErrors.IdentityUsernameNotUnique);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Õåøèðîâàíèå ïàðîëÿ
|
// 2. ����������� ������
|
||||||
string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password);
|
string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password);
|
||||||
|
|
||||||
// 3. Ñîçäàíèå ñóùíîñòè
|
// 3. �������� ��������
|
||||||
var user = User.Create(
|
var user = User.Create(
|
||||||
request.Username,
|
request.Username,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
@@ -65,7 +65,7 @@ internal sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserC
|
|||||||
request.Email,
|
request.Email,
|
||||||
request.Bio);
|
request.Bio);
|
||||||
|
|
||||||
// 4. Ñîõðàíåíèå - èñïîëüçóåì ìåòîä ñ Domain User
|
// 4. ���������� - ���������� ����� � Domain User
|
||||||
var repoWithDomainUserAdd = _userRepository as Infrastructure.Persistence.UserRepository;
|
var repoWithDomainUserAdd = _userRepository as Infrastructure.Persistence.UserRepository;
|
||||||
repoWithDomainUserAdd?.Add(user);
|
repoWithDomainUserAdd?.Add(user);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IAuthUnitOfWork>(sp => sp.GetRequiredService<AuthDbContext>());
|
services.AddScoped<IAuthUnitOfWork>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||||
services.AddScoped<Knot.Contracts.Auth.Application.Abstractions.IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
services.AddScoped<Knot.Contracts.Auth.Application.Abstractions.IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||||
services.AddScoped<Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
services.AddScoped<Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||||
services.AddScoped<IUserRepository, UserRepository>();
|
services.AddScoped<Knot.Contracts.Auth.Domain.IUserRepository, UserRepository>();
|
||||||
services.AddScoped<IJwtTokenProvider, JwtTokenProvider>();
|
services.AddScoped<IJwtTokenProvider, JwtTokenProvider>();
|
||||||
services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Auth.Infrastructure.Services.UserDisplayNameProvider>();
|
services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Auth.Infrastructure.Services.UserDisplayNameProvider>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Domain;
|
||||||
|
|
||||||
|
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");
|
||||||
|
public static readonly Error FriendsExists = new Error("Friends.Exists", "Friendship already exists");
|
||||||
|
public static readonly Error UserNotFound = new Error("User.NotFound", "User not found");
|
||||||
|
public static readonly Error IdentityInvalidCredentials = new Error("Identity.InvalidCredentials", "Неверное имя пользователя или пароль.");
|
||||||
|
public static readonly Error IdentityRegistrationDisabled = new Error("Identity.RegistrationDisabled", "Registration is disabled by the administrator.");
|
||||||
|
public static readonly Error IdentityUsernameNotUnique = new Error("Identity.UsernameNotUnique", "Это имя пользователя уже занято.");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -28,8 +28,14 @@ public sealed class User : AggregateRoot<Guid>
|
|||||||
public string? RefreshToken { get; private set; }
|
public string? RefreshToken { get; private set; }
|
||||||
public DateTime? BannedUntil { get; private set; }
|
public DateTime? BannedUntil { get; private set; }
|
||||||
|
|
||||||
public void Ban() => IsBanned = true;
|
public void Ban() {
|
||||||
public void Unban() => IsBanned = false;
|
IsBanned = true;
|
||||||
|
RaiseDomainEvent(new UserBannedDomainEvent(Id, true));
|
||||||
|
}
|
||||||
|
public void Unban() {
|
||||||
|
IsBanned = false;
|
||||||
|
RaiseDomainEvent(new UserBannedDomainEvent(Id, false));
|
||||||
|
}
|
||||||
|
|
||||||
public void SetOnline(bool isOnline, DateTime? lastSeen = null)
|
public void SetOnline(bool isOnline, DateTime? lastSeen = null)
|
||||||
{
|
{
|
||||||
@@ -66,6 +72,9 @@ public sealed class User : AggregateRoot<Guid>
|
|||||||
Username = contract.Username;
|
Username = contract.Username;
|
||||||
DisplayName = contract.DisplayName;
|
DisplayName = contract.DisplayName;
|
||||||
PhoneNumber = contract.PhoneNumber;
|
PhoneNumber = contract.PhoneNumber;
|
||||||
|
Bio = contract.Bio;
|
||||||
|
Avatar = contract.Avatar;
|
||||||
|
Birthday = contract.Birthday;
|
||||||
IsBanned = contract.IsBanned;
|
IsBanned = contract.IsBanned;
|
||||||
BannedUntil = contract.BannedUntil;
|
BannedUntil = contract.BannedUntil;
|
||||||
SetOnline(contract.IsOnline, contract.LastSeen);
|
SetOnline(contract.IsOnline, contract.LastSeen);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Глобальные алиасы для модуля Auth
|
||||||
|
// Используем контракты вместо дублирующихся интерфейсов из модуля
|
||||||
|
global using AuthErrors = Knot.Contracts.Auth.Application.Auth.DTOs.AuthErrors;
|
||||||
|
global using IUserRepository = Knot.Contracts.Auth.Domain.IUserRepository;
|
||||||
@@ -65,6 +65,7 @@ internal sealed class UserRepository : IUserRepository
|
|||||||
{
|
{
|
||||||
domainUser.UpdateFromContract(user);
|
domainUser.UpdateFromContract(user);
|
||||||
_context.Users.Update(domainUser);
|
_context.Users.Update(domainUser);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using Knot.Contracts.Auth.Domain;
|
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
namespace Knot.Modules.Auth.Infrastructure.Services;
|
namespace Knot.Modules.Auth.Infrastructure.Services;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
<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="MediatR" Version="12.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
// <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;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AuthDbContext))]
|
||||||
|
[Migration("20260407181656_AddUserInfoFields")]
|
||||||
|
partial class AddUserInfoFields
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(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<DateTime?>("BannedUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
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>("Domain")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("HideStatus")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("HideStoryViews")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBanned")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsExternal")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOnline")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastSeen")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserDomain")
|
||||||
|
.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,37 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserInfoFields : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='identity' AND table_name='Users' AND column_name='BannedUntil') THEN
|
||||||
|
ALTER TABLE identity.""Users"" ADD ""BannedUntil"" timestamp with time zone;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='identity' AND table_name='Users' AND column_name='PhoneNumber') THEN
|
||||||
|
ALTER TABLE identity.""Users"" ADD ""PhoneNumber"" text;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='identity' AND table_name='Users' AND column_name='RefreshToken') THEN
|
||||||
|
ALTER TABLE identity.""Users"" ADD ""RefreshToken"" text;
|
||||||
|
END IF;
|
||||||
|
END $$;");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(name: "BannedUntil", schema: "identity", table: "Users");
|
||||||
|
migrationBuilder.DropColumn(name: "PhoneNumber", schema: "identity", table: "Users");
|
||||||
|
migrationBuilder.DropColumn(name: "RefreshToken", schema: "identity", table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AuthDbContext))]
|
||||||
|
[Migration("20270328000000_AddUserUserDomainField")]
|
||||||
|
partial class AddUserUserDomainField
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(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<DateTime?>("BannedUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Birthday")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Domain")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("HideStatus")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("HideStoryViews")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBanned")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsExternal")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOnline")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastSeen")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserDomain")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", "identity");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserUserDomainField : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "UserDomain",
|
||||||
|
schema: "identity",
|
||||||
|
table: "Users",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UserDomain",
|
||||||
|
schema: "identity",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -23,7 +23,7 @@ namespace Knot.Modules.Auth.Migrations
|
|||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Knot.Contracts.Auth.Domain.User", b =>
|
modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -32,6 +32,9 @@ namespace Knot.Modules.Auth.Migrations
|
|||||||
b.Property<string>("Avatar")
|
b.Property<string>("Avatar")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("BannedUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Bio")
|
b.Property<string>("Bio")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
@@ -57,10 +60,10 @@ namespace Knot.Modules.Auth.Migrations
|
|||||||
b.Property<bool>("HideStoryViews")
|
b.Property<bool>("HideStoryViews")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("IsExternal")
|
b.Property<bool>("IsBanned")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("IsBanned")
|
b.Property<bool>("IsExternal")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("IsOnline")
|
b.Property<bool>("IsOnline")
|
||||||
@@ -73,6 +76,15 @@ namespace Knot.Modules.Auth.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserDomain")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Username")
|
b.Property<string>("Username")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using Carter;
|
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Modules.Auth.Application.Users.Login;
|
using Knot.Modules.Auth.Application.Users.Login;
|
||||||
using Knot.Modules.Auth.Application.Users.Register;
|
using Knot.Modules.Auth.Application.Users.Register;
|
||||||
using Knot.Modules.Auth.Application.Users.GetMe;
|
using Knot.Modules.Auth.Application.Users.GetMe;
|
||||||
|
using Knot.Modules.Auth.Application.Users.ChangePassword;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -11,9 +11,11 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace Knot.Modules.Auth.Presentation.Endpoints;
|
namespace Knot.Modules.Auth.Presentation.Endpoints;
|
||||||
|
|
||||||
public sealed class AuthEndpoints : ICarterModule
|
public static class AuthEndpoints
|
||||||
{
|
{
|
||||||
public void AddRoutes(IEndpointRouteBuilder app)
|
public sealed record ChangePasswordRequest(string OldPassword, string NewPassword, string ConfirmPassword);
|
||||||
|
|
||||||
|
public static void MapAuthEndpoints(this WebApplication app)
|
||||||
{
|
{
|
||||||
var group = app.MapGroup("api/auth");
|
var group = app.MapGroup("api/auth");
|
||||||
|
|
||||||
@@ -34,5 +36,19 @@ public sealed class AuthEndpoints : ICarterModule
|
|||||||
var result = await sender.Send(new GetMeQuery(userContext.UserId), ct);
|
var result = await sender.Send(new GetMeQuery(userContext.UserId), ct);
|
||||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
|
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
|
||||||
}).RequireAuthorization();
|
}).RequireAuthorization();
|
||||||
|
|
||||||
|
group.MapPost("change-password", async ([FromBody] ChangePasswordRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var result = await sender.Send(
|
||||||
|
new ChangePasswordCommand(userContext.UserId, request.OldPassword, request.NewPassword, request.ConfirmPassword),
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (result.IsSuccess) return Results.Ok();
|
||||||
|
|
||||||
|
if (result.Error.Code == "Auth.OldPasswordInvalid")
|
||||||
|
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||||
|
|
||||||
|
return Results.BadRequest(new { error = result.Error.Code ?? result.Error.Description });
|
||||||
|
}).RequireAuthorization();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
using Knot.Shared.Kernel;
|
|
||||||
|
|
||||||
namespace Knot.Modules.Conversations.Application.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unit of Work специфичный для модуля Chats.
|
|
||||||
/// </summary>
|
|
||||||
public interface IChatsUnitOfWork : IUnitOfWork
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Knot.Modules.Conversations.Application.Abstractions;
|
|
||||||
|
|
||||||
public interface IUserStatusService
|
|
||||||
{
|
|
||||||
bool IsUserOnline(string userId);
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,8 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Shared.Kernel.Storage;
|
using Knot.Shared.Kernel.Storage;
|
||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ using System;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
|
|
||||||
namespace Knot.Modules.Conversations.Application.Chats.Create;
|
namespace Knot.Modules.Conversations.Application.Chats.Create;
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
using Knot.Contracts.Messaging.Domain;
|
using Knot.Contracts.Messaging.Domain;
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using Knot.Modules.Conversations.Application.DTOs;
|
using Knot.Modules.Conversations.Application.DTOs;
|
||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -63,6 +63,9 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pinnedMessages = await _messageRepository.GetPinnedMessagesAsync(chat.Id, cancellationToken);
|
||||||
|
foreach (var pm in pinnedMessages) userIdsToFetch.Add(pm.SenderId);
|
||||||
|
|
||||||
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
||||||
|
|
||||||
var members = new List<ChatMemberDto>();
|
var members = new List<ChatMemberDto>();
|
||||||
@@ -88,51 +91,19 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
|||||||
var messagesList = new List<ChatMessageDto>();
|
var messagesList = new List<ChatMessageDto>();
|
||||||
if (latestMessage != null)
|
if (latestMessage != null)
|
||||||
{
|
{
|
||||||
usersInfo.TryGetValue(latestMessage.SenderId, out var senderObj);
|
messagesList.Add(MessageMapper.MapToDto(
|
||||||
|
latestMessage,
|
||||||
var reactionsWithUser = new List<ReactionDto>();
|
usersInfo,
|
||||||
foreach (var reaction in latestReactions)
|
latestReactions,
|
||||||
{
|
chat.Members.Where(m => m.LastReadSequenceId >= latestMessage.SequenceId && m.UserId != latestMessage.SenderId).Select(m => m.UserId)));
|
||||||
usersInfo.TryGetValue(reaction.UserId, out var reactionUser);
|
|
||||||
reactionsWithUser.Add(new ReactionDto(
|
|
||||||
reaction.Id,
|
|
||||||
reaction.Emoji,
|
|
||||||
reaction.UserId,
|
|
||||||
reactionUser != null
|
|
||||||
? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar)
|
|
||||||
: new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var readByList = chat.Members
|
var pinnedDtoList = new List<PinnedMessageDto>();
|
||||||
.Where(m => m.LastReadSequenceId >= latestMessage.SequenceId && m.UserId != latestMessage.SenderId)
|
foreach (var pm in pinnedMessages)
|
||||||
.Select(m => new ReadByDto(m.UserId))
|
{
|
||||||
.ToList();
|
pinnedDtoList.Add(new PinnedMessageDto(
|
||||||
|
pm.Id,
|
||||||
messagesList.Add(new ChatMessageDto(
|
MessageMapper.MapToDto(pm, usersInfo, new List<MessageReaction>(), new List<Guid>())
|
||||||
latestMessage.Id,
|
|
||||||
latestMessage.ChatId,
|
|
||||||
latestMessage.SenderId,
|
|
||||||
latestMessage.Content,
|
|
||||||
latestMessage.Type,
|
|
||||||
latestMessage.ReplyToId,
|
|
||||||
latestMessage.Quote,
|
|
||||||
latestMessage.StoryId,
|
|
||||||
latestMessage.StoryMediaUrl,
|
|
||||||
latestMessage.StoryMediaType,
|
|
||||||
latestMessage.IsEdited,
|
|
||||||
latestMessage.IsDeleted,
|
|
||||||
latestMessage.CreatedAt,
|
|
||||||
latestMessage.SequenceId,
|
|
||||||
latestMessage.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
|
|
||||||
senderObj != null ? new MessageSenderDto(
|
|
||||||
senderObj.Id,
|
|
||||||
senderObj.Username,
|
|
||||||
senderObj.DisplayName,
|
|
||||||
senderObj.Avatar
|
|
||||||
) : new MessageSenderDto(latestMessage.SenderId, "unknown", "Unknown", null),
|
|
||||||
reactionsWithUser,
|
|
||||||
readByList
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +119,7 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
|||||||
chat.CreatedAt,
|
chat.CreatedAt,
|
||||||
members,
|
members,
|
||||||
messagesList,
|
messagesList,
|
||||||
|
pinnedDtoList,
|
||||||
unreadCount
|
unreadCount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user