Сборка бэк
This commit is contained in:
@@ -14,7 +14,7 @@ using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record CleanRunCommand(IFileStorageService FileStorage, AuthDbContext IdentityDb) : ICommand<MessageResponse>;
|
||||
|
||||
@@ -99,4 +99,4 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
||||
|
||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record ResetUserPasswordCommand(Guid UserId, string NewPassword) : ICommand<SuccessResponse>;
|
||||
|
||||
@@ -44,4 +44,4 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUse
|
||||
await _identityUnitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(new SuccessResponse(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
|
||||
namespace Host.Application.Admin.Commands.TestKlipy;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||
|
||||
public record TestKlipyConnectionCommand() : ICommand<bool>;
|
||||
|
||||
@@ -37,3 +38,5 @@ internal sealed class TestKlipyConnectionCommandHandler : ICommandHandler<TestKl
|
||||
return Result.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record UpdateSettingsCommand(SystemSettingsDto Settings) : ICommand<SystemSettingsDto>;
|
||||
|
||||
@@ -43,4 +44,6 @@ internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSetti
|
||||
|
||||
return Result.Success(request.Settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
public record CleanDryRunQuery(IFileStorageService FileStorage, AuthDbContext IdentityDb) : IQuery<CleanupDryRunResultDto>;
|
||||
|
||||
@@ -102,4 +102,4 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
||||
OrphanedMediaBytes = safeBytes
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Services;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
public record GetDashboardStatsQuery() : IQuery<DashboardStatsDto>;
|
||||
|
||||
@@ -23,4 +23,4 @@ internal sealed class GetDashboardStatsQueryHandler : IQueryHandler<GetDashboard
|
||||
var stats = await _statisticsService.GetDashboardStatsAsync(cancellationToken);
|
||||
return Result.Success(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
public record GetSettingsQuery() : IQuery<SystemSettingsDto>;
|
||||
|
||||
@@ -24,4 +25,5 @@ internal sealed class GetSettingsQueryHandler : IQueryHandler<GetSettingsQuery,
|
||||
var settings = await _settingsService.GetSettingsAsync(cancellationToken);
|
||||
return Result.Success(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
public record GetUserDetailsQuery(Guid UserId) : IQuery<AdminUserDetailsDto>;
|
||||
|
||||
@@ -74,4 +74,4 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
public record SearchUsersQuery(string Query) : IQuery<List<AdminUserDto>>;
|
||||
|
||||
@@ -43,4 +43,4 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery,
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Knot.Modules.Admin.Domain.Events;
|
||||
|
||||
@@ -8,3 +9,4 @@ namespace Knot.Modules.Admin.Domain.Events;
|
||||
/// Доменное событие: глобальные настройки системы обновлены.
|
||||
/// </summary>
|
||||
public sealed record SystemSettingsUpdatedDomainEvent(SystemSettingsDto Settings) : IDomainEvent;
|
||||
|
||||
|
||||
@@ -18,5 +18,7 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>Knot.Modules.Admin.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
using Carter;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Auth.Application.Users.Register;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using MediatR;
|
||||
using Host.Application.Admin.Queries;
|
||||
using Host.Application.Admin.Commands;
|
||||
using Knot.Modules.Admin.Application.Admin.Queries;
|
||||
using Knot.Modules.Admin.Application.Admin.Commands;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Host.Application.Admin.Commands.TestKlipy;
|
||||
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
|
||||
@@ -90,4 +91,5 @@ public sealed class AdminEndpoints : ICarterModule
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Auth.Application.Users.Auth;
|
||||
using BCrypt.Net;
|
||||
|
||||
@@ -87,3 +88,4 @@ public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCom
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Events;
|
||||
|
||||
namespace Knot.Modules.Auth.Domain;
|
||||
|
||||
@@ -40,7 +41,9 @@ public sealed class User : AggregateRoot<Guid>
|
||||
/// </summary>
|
||||
public static User Create(string username, string passwordHash, string displayName, string? email = null, string? bio = null)
|
||||
{
|
||||
return new User(Guid.NewGuid(), username, passwordHash, displayName, email, bio);
|
||||
var user = new User(Guid.NewGuid(), username, passwordHash, displayName, email, bio);
|
||||
user.RaiseDomainEvent(new UserRegisteredDomainEvent(user.Id, user.Username, user.DisplayName, user.Bio));
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -21,13 +21,17 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
@@ -48,3 +49,4 @@ internal sealed class AddToFolderCommandHandler : ICommandHandler<AddToFolderCom
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Send;
|
||||
@@ -167,3 +168,4 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.UploadFile;
|
||||
@@ -45,3 +46,4 @@ internal sealed class UploadFileCommandHandler : ICommandHandler<UploadFileComma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.7.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Knot.Modules.Chats.UnitTests" />
|
||||
<InternalsVisibleTo Include="Knot.Modules.Conversations.UnitTests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -36,3 +36,5 @@
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ using Knot.Shared.Kernel.Constants;
|
||||
using System.Security.Cryptography;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Host.Application.Federation.Commands;
|
||||
|
||||
@@ -63,4 +64,4 @@ internal sealed class HandshakeFederationCommandHandler : ICommandHandler<Handsh
|
||||
|
||||
return Result.Success(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
|
||||
namespace Host.Application.Federation.Commands;
|
||||
@@ -204,3 +205,4 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using System.Linq;
|
||||
|
||||
namespace Host.Application.Federation.Commands;
|
||||
@@ -38,3 +39,4 @@ internal sealed class ResolveUserCommandHandler : ICommandHandler<ResolveUserCom
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Federation.Application.Federation.Events;
|
||||
@@ -95,3 +96,4 @@ public sealed class FederatedMessageActionsHandler :
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Knot.Modules.Federation.Application.Federation.Events;
|
||||
@@ -88,3 +89,4 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Modules.Admin.Domain.Events;
|
||||
using Knot.Modules.Settings.Domain.Events;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Knot.Modules.Federation.Application.Federation.Events;
|
||||
|
||||
@@ -69,3 +70,5 @@ public sealed class SystemSettingsUpdatedDomainEventHandler : INotificationHandl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Knot.Modules.Federation.Application.Federation.Events;
|
||||
|
||||
@@ -84,3 +85,4 @@ public sealed class UserStatusChangedDomainEventHandler : INotificationHandler<U
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
|
||||
namespace Knot.Modules.Federation.Application.Federation.Services;
|
||||
@@ -105,3 +106,4 @@ public sealed class FederationPacketService
|
||||
return Result.Success(packet);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
|
||||
namespace Knot.Modules.Federation.Infrastructure.Services;
|
||||
@@ -56,3 +57,4 @@ public sealed class FederationGateway : IFederationGateway
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,5 +13,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ using System;
|
||||
using Host.Application.Federation.Commands;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
|
||||
@@ -63,4 +64,4 @@ public sealed class FederationEndpoints : ICarterModule
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System;
|
||||
@@ -61,3 +62,4 @@ internal sealed class GetTrendingGifsQueryHandler : IQueryHandler<GetTrendingGif
|
||||
return Result.Success<JsonElement?>(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System;
|
||||
@@ -65,3 +66,4 @@ internal sealed class SearchGifsQueryHandler : IQueryHandler<SearchGifsQuery, Js
|
||||
return Result.Success<JsonElement?>(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,5 +13,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
@@ -7,7 +7,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -17,3 +18,5 @@
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс для хранения аватаров в S3 (MinIO).
|
||||
/// Отдельный от IFileStorageService модуля Storage,
|
||||
/// чтобы не создавать прямой зависимости на Storage модуль.
|
||||
/// </summary>
|
||||
public interface IAvatarStorageService
|
||||
{
|
||||
/// <summary>
|
||||
/// Загружает файл в S3-бакет аватаров и возвращает fileId (ключ объекта).
|
||||
/// </summary>
|
||||
Task<string> UploadAsync(System.IO.Stream stream, string fileName, string contentType, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет файл аватара из S3 по fileId.
|
||||
/// </summary>
|
||||
Task DeleteAsync(string fileId, CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
namespace Knot.Modules.Profiles.Application.Abstractions;
|
||||
|
||||
public interface IProfileRepository
|
||||
{
|
||||
Task<Profile?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(Profile profile, CancellationToken cancellationToken = default);
|
||||
void Update(Profile profile);
|
||||
Task<System.Collections.Generic.List<Profile>> SearchProfilesAsync(string query, System.Threading.CancellationToken ct = default);
|
||||
Task<ProfileDocument?> GetByIdAsync(Guid userId, CancellationToken ct = default);
|
||||
Task AddAsync(ProfileDocument profile, CancellationToken ct = default);
|
||||
Task UpdateAsync(ProfileDocument profile, CancellationToken ct = default);
|
||||
Task<List<ProfileDocument>> SearchAsync(string query, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
namespace Knot.Modules.Profiles.Application.Abstractions;
|
||||
|
||||
public interface IProfilesUnitOfWork
|
||||
{
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,71 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.Avatar;
|
||||
|
||||
public sealed record CropAvatarCommand(Guid ProfileId, Stream FileStream, string FileName, string ContentType, int X, int Y, int Width, int Height) : ICommand<ProfileProfileDto>;
|
||||
public sealed record CropAvatarCommand(
|
||||
Guid UserId,
|
||||
Stream FileStream,
|
||||
string FileName,
|
||||
string ContentType,
|
||||
int X, int Y, int Width, int Height) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarCommand, ProfileProfileDto>
|
||||
internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly IProfilesUnitOfWork _unitOfWork;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IProfileRepository _repository;
|
||||
private readonly IAvatarStorageService _avatarStorage;
|
||||
|
||||
public CropAvatarCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork, IFileStorageService fileStorage)
|
||||
public CropAvatarCommandHandler(IProfileRepository repository, IAvatarStorageService avatarStorage)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_fileStorage = fileStorage;
|
||||
_repository = repository;
|
||||
_avatarStorage = avatarStorage;
|
||||
}
|
||||
|
||||
public async Task<Result<ProfileProfileDto>> Handle(CropAvatarCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Result<UserProfileDto>> Handle(CropAvatarCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken);
|
||||
if (profile == null)
|
||||
{
|
||||
return Result.Failure<ProfileProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
}
|
||||
var profile = await _repository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (profile is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
string avatarUrl;
|
||||
// Обрезать и ресайзнуть изображение до 400×400
|
||||
using var ms = await CropAndResizeAsync(request, cancellationToken);
|
||||
|
||||
using (var image = await SixLabors.ImageSharp.Image.LoadAsync(request.FileStream, cancellationToken))
|
||||
{
|
||||
int startX = Math.Max(0, Math.Min(request.X, image.Width - 1));
|
||||
int startY = Math.Max(0, Math.Min(request.Y, image.Height - 1));
|
||||
int rectWidth = Math.Max(1, Math.Min(request.Width, image.Width - startX));
|
||||
int rectHeight = Math.Max(1, Math.Min(request.Height, image.Height - startY));
|
||||
// Удалить старый аватар из S3
|
||||
if (!string.IsNullOrEmpty(profile.AvatarUrl))
|
||||
await _avatarStorage.DeleteAsync(profile.AvatarUrl, cancellationToken);
|
||||
|
||||
image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight)));
|
||||
image.Mutate(ctx => ctx.Resize(400, 400));
|
||||
|
||||
using var outStream = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(outStream, cancellationToken);
|
||||
outStream.Position = 0;
|
||||
|
||||
var id = await _fileStorage.UploadFileAsync(outStream, request.FileName, "image/jpeg");
|
||||
avatarUrl = $"/api/files/{id}";
|
||||
}
|
||||
var fileId = await _avatarStorage.UploadAsync(ms, "avatar.jpg", "image/jpeg", cancellationToken);
|
||||
var avatarUrl = $"/api/files/{fileId}";
|
||||
|
||||
profile.UpdateAvatar(avatarUrl);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
await _repository.UpdateAsync(profile, cancellationToken);
|
||||
|
||||
var dto = new ProfileProfileDto(
|
||||
profile.Id,
|
||||
profile.Profilename,
|
||||
profile.DisplayName,
|
||||
profile.Avatar,
|
||||
profile.Bio,
|
||||
profile.Birthday,
|
||||
profile.CreatedAt
|
||||
);
|
||||
return Result.Success(UserProfileDto.FromDocument(profile));
|
||||
}
|
||||
|
||||
return Result.Success(dto);
|
||||
private static async Task<MemoryStream> CropAndResizeAsync(CropAvatarCommand request, CancellationToken ct)
|
||||
{
|
||||
using var image = await Image.LoadAsync(request.FileStream, ct);
|
||||
|
||||
var startX = Math.Clamp(request.X, 0, image.Width - 1);
|
||||
var startY = Math.Clamp(request.Y, 0, image.Height - 1);
|
||||
var width = Math.Clamp(request.Width, 1, image.Width - startX);
|
||||
var height = Math.Clamp(request.Height, 1, image.Height - startY);
|
||||
|
||||
image.Mutate(ctx => ctx
|
||||
.Crop(new Rectangle(startX, startY, width, height))
|
||||
.Resize(400, 400));
|
||||
|
||||
var output = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(output, ct);
|
||||
output.Position = 0;
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.Avatar;
|
||||
|
||||
public sealed record DeleteAvatarCommand(Guid ProfileId) : ICommand<ProfileProfileDto>;
|
||||
|
||||
internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarCommand, ProfileProfileDto>
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly IProfilesUnitOfWork _unitOfWork;
|
||||
|
||||
public DeleteAvatarCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<ProfileProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken);
|
||||
if (profile == null)
|
||||
{
|
||||
return Result.Failure<ProfileProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
}
|
||||
|
||||
profile.UpdateAvatar(null);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var dto = new ProfileProfileDto(
|
||||
profile.Id,
|
||||
profile.Profilename,
|
||||
profile.DisplayName,
|
||||
profile.Avatar,
|
||||
profile.Bio,
|
||||
profile.Birthday,
|
||||
profile.CreatedAt
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,79 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.Avatar;
|
||||
|
||||
public sealed record UploadAvatarCommand(Guid ProfileId, Stream FileStream, string FileName, string ContentType) : ICommand<ProfileProfileDto>;
|
||||
// ─── Upload ────────────────────────────────────────────────────────────────
|
||||
|
||||
internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarCommand, ProfileProfileDto>
|
||||
public sealed record UploadAvatarCommand(
|
||||
Guid UserId,
|
||||
Stream FileStream,
|
||||
string FileName,
|
||||
string ContentType) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly IProfilesUnitOfWork _unitOfWork;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IProfileRepository _repository;
|
||||
private readonly IAvatarStorageService _avatarStorage;
|
||||
|
||||
public UploadAvatarCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork, IFileStorageService fileStorage)
|
||||
public UploadAvatarCommandHandler(IProfileRepository repository, IAvatarStorageService avatarStorage)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_fileStorage = fileStorage;
|
||||
_repository = repository;
|
||||
_avatarStorage = avatarStorage;
|
||||
}
|
||||
|
||||
public async Task<Result<ProfileProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Result<UserProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken);
|
||||
if (profile == null)
|
||||
{
|
||||
return Result.Failure<ProfileProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
}
|
||||
var profile = await _repository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (profile is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
|
||||
// Удалить старый аватар из S3, если был
|
||||
if (!string.IsNullOrEmpty(profile.AvatarUrl))
|
||||
await _avatarStorage.DeleteAsync(profile.AvatarUrl, cancellationToken);
|
||||
|
||||
var fileId = await _avatarStorage.UploadAsync(request.FileStream, request.FileName, request.ContentType, cancellationToken);
|
||||
var avatarUrl = $"/api/files/{fileId}";
|
||||
|
||||
profile.UpdateAvatar(avatarUrl);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
await _repository.UpdateAsync(profile, cancellationToken);
|
||||
|
||||
var dto = new ProfileProfileDto(
|
||||
profile.Id,
|
||||
profile.Profilename,
|
||||
profile.DisplayName,
|
||||
profile.Avatar,
|
||||
profile.Bio,
|
||||
profile.Birthday,
|
||||
profile.CreatedAt
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
return Result.Success(UserProfileDto.FromDocument(profile));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Delete ────────────────────────────────────────────────────────────────
|
||||
|
||||
public sealed record DeleteAvatarCommand(Guid UserId) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IProfileRepository _repository;
|
||||
private readonly IAvatarStorageService _avatarStorage;
|
||||
|
||||
public DeleteAvatarCommandHandler(IProfileRepository repository, IAvatarStorageService avatarStorage)
|
||||
{
|
||||
_repository = repository;
|
||||
_avatarStorage = avatarStorage;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await _repository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (profile is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
if (!string.IsNullOrEmpty(profile.AvatarUrl))
|
||||
await _avatarStorage.DeleteAsync(profile.AvatarUrl, cancellationToken);
|
||||
|
||||
profile.RemoveAvatar();
|
||||
await _repository.UpdateAsync(profile, cancellationToken);
|
||||
|
||||
return Result.Success(UserProfileDto.FromDocument(profile));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
|
||||
public record UpdateProfileRequest(string? DisplayName, string? Bio, DateTime? Birthday);
|
||||
public record UpdateSettingsRequest(bool? HideStoryViews);
|
||||
@@ -1,7 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
|
||||
public sealed record UpdateProfileRequest(string? DisplayName, string? Bio, DateTime? Birthday);
|
||||
@@ -1,5 +0,0 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
|
||||
public sealed record UpdateSettingsRequest(bool? HideStoryViews);
|
||||
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
|
||||
public record ProfileDto(
|
||||
Guid Id,
|
||||
string Profilename,
|
||||
string DisplayName,
|
||||
string? Avatar,
|
||||
bool IsOnline,
|
||||
DateTime LastSeen
|
||||
);
|
||||
@@ -1,18 +1,29 @@
|
||||
using System;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
|
||||
public record ProfileProfileDto(
|
||||
public record UserProfileDto(
|
||||
Guid Id,
|
||||
string Profilename,
|
||||
string UserName,
|
||||
string DisplayName,
|
||||
string? Avatar,
|
||||
string? AvatarUrl,
|
||||
string? Bio,
|
||||
DateTime? Birthday,
|
||||
DateTime CreatedAt,
|
||||
bool? HideStoryViews = null,
|
||||
bool HideStoryViews = false,
|
||||
bool IsOnline = false,
|
||||
DateTime? LastSeen = null
|
||||
);
|
||||
DateTime? LastSeen = null)
|
||||
{
|
||||
public static UserProfileDto FromDocument(ProfileDocument doc) =>
|
||||
new(
|
||||
doc.Id,
|
||||
doc.Username,
|
||||
doc.DisplayName,
|
||||
doc.AvatarUrl,
|
||||
doc.Bio,
|
||||
doc.Birthday,
|
||||
doc.CreatedAt,
|
||||
doc.HideStoryViews
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.GetProfile;
|
||||
|
||||
public sealed record GetProfileQuery(Guid Id) : IQuery<ProfileProfileDto>;
|
||||
public sealed record GetProfileQuery(Guid UserId) : IQuery<UserProfileDto>;
|
||||
|
||||
internal sealed class GetProfileQueryHandler : IQueryHandler<GetProfileQuery, ProfileProfileDto>
|
||||
internal sealed class GetProfileQueryHandler : IQueryHandler<GetProfileQuery, UserProfileDto>
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly IProfileRepository _repository;
|
||||
|
||||
public GetProfileQueryHandler(IProfileRepository profileRepository)
|
||||
public GetProfileQueryHandler(IProfileRepository repository)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Result<ProfileProfileDto>> Handle(GetProfileQuery request, CancellationToken cancellationToken)
|
||||
public async Task<Result<UserProfileDto>> Handle(GetProfileQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await _profileRepository.GetByIdAsync(request.Id, cancellationToken);
|
||||
if (profile == null)
|
||||
{
|
||||
return Result.Failure<ProfileProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
}
|
||||
var profile = await _repository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (profile is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
var dto = new ProfileProfileDto(
|
||||
profile.Id,
|
||||
profile.Profilename,
|
||||
profile.Name,
|
||||
profile.AvatarUrl
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
return Result.Success(UserProfileDto.FromDocument(profile));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
using Knot.Shared.Kernel.Events;
|
||||
using MediatR;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Реакция модуля Profiles на создание пользователя в модуле Auth.
|
||||
/// Создает соответствующий документ в MongoDB.
|
||||
/// </summary>
|
||||
internal sealed class UserRegisteredDomainEventHandler : INotificationHandler<UserRegisteredDomainEvent>
|
||||
{
|
||||
private readonly IProfileRepository _repository;
|
||||
|
||||
public UserRegisteredDomainEventHandler(IProfileRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task Handle(UserRegisteredDomainEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
// Проверяем, существует ли уже профиль (защита от дублей)
|
||||
var existing = await _repository.GetByIdAsync(notification.UserId, cancellationToken);
|
||||
if (existing is not null) return;
|
||||
|
||||
var profile = ProfileDocument.Create(
|
||||
notification.UserId,
|
||||
notification.Username,
|
||||
notification.DisplayName,
|
||||
notification.Bio
|
||||
);
|
||||
|
||||
await _repository.AddAsync(profile, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,28 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.Search;
|
||||
|
||||
public sealed record SearchProfilesQuery(string Query) : IQuery<List<ProfileDto>>;
|
||||
public sealed record SearchProfilesQuery(string Query) : IQuery<List<UserProfileDto>>;
|
||||
|
||||
internal sealed class SearchProfilesQueryHandler : IQueryHandler<SearchProfilesQuery, List<ProfileDto>>
|
||||
internal sealed class SearchProfilesQueryHandler : IQueryHandler<SearchProfilesQuery, List<UserProfileDto>>
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly IProfileRepository _repository;
|
||||
|
||||
public SearchProfilesQueryHandler(IProfileRepository profileRepository)
|
||||
public SearchProfilesQueryHandler(IProfileRepository repository)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<ProfileDto>>> Handle(SearchProfilesQuery request, CancellationToken cancellationToken)
|
||||
public async Task<Result<List<UserProfileDto>>> Handle(SearchProfilesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profiles = await _profileRepository.SearchProfilesAsync(request.Query, cancellationToken);
|
||||
|
||||
var result = profiles.Select(profile => new ProfileDto(
|
||||
profile.Id,
|
||||
profile.Profilename,
|
||||
profile.DisplayName,
|
||||
profile.Avatar,
|
||||
false,
|
||||
DateTime.UtcNow
|
||||
)).ToList();
|
||||
|
||||
return Result.Success(result);
|
||||
var profiles = await _repository.SearchAsync(request.Query, cancellationToken);
|
||||
var dtos = profiles.Select(UserProfileDto.FromDocument).ToList();
|
||||
return Result.Success(dtos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.UpdateProfile;
|
||||
|
||||
public sealed record UpdateProfileCommand(Guid UserId, string? DisplayName, string? Bio, DateTime? Birthday) : ICommand<UserProfileDto>;
|
||||
public sealed record UpdateProfileCommand(
|
||||
Guid UserId,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
DateTime? Birthday) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class UpdateProfileCommandHandler : ICommandHandler<UpdateProfileCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IProfileRepository _userRepository;
|
||||
private readonly IProfilesUnitOfWork _unitOfWork;
|
||||
private readonly IProfileRepository _repository;
|
||||
|
||||
public UpdateProfileCommandHandler(IProfileRepository userRepository, IProfilesUnitOfWork unitOfWork)
|
||||
public UpdateProfileCommandHandler(IProfileRepository repository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(UpdateProfileCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
var profile = await _repository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (profile is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
user.UpdateProfile(request.DisplayName ?? user.DisplayName, request.Bio, request.Birthday);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
profile.UpdateProfile(
|
||||
request.DisplayName ?? profile.DisplayName,
|
||||
request.Bio,
|
||||
request.Birthday);
|
||||
|
||||
var dto = new UserProfileDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
user.Bio,
|
||||
user.Birthday,
|
||||
user.CreatedAt
|
||||
);
|
||||
await _repository.UpdateAsync(profile, cancellationToken);
|
||||
|
||||
return Result.Success(dto);
|
||||
return Result.Success(UserProfileDto.FromDocument(profile));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.UpdateSettings;
|
||||
|
||||
public sealed record UpdateSettingsCommand(Guid ProfileId, bool? HideStoryViews) : ICommand<ProfileProfileDto>;
|
||||
public sealed record UpdateSettingsCommand(Guid UserId, bool? HideStoryViews) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSettingsCommand, ProfileProfileDto>
|
||||
internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSettingsCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly IProfilesUnitOfWork _unitOfWork;
|
||||
private readonly IProfileRepository _repository;
|
||||
|
||||
public UpdateSettingsCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork)
|
||||
public UpdateSettingsCommandHandler(IProfileRepository repository)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Result<ProfileProfileDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Result<UserProfileDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken);
|
||||
if (profile == null)
|
||||
{
|
||||
return Result.Failure<ProfileProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
}
|
||||
var profile = await _repository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (profile is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
profile.UpdateSettings(request.HideStoryViews ?? profile.HideStoryViews);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
await _repository.UpdateAsync(profile, cancellationToken);
|
||||
|
||||
var dto = new ProfileProfileDto(
|
||||
profile.Id,
|
||||
profile.Profilename,
|
||||
profile.Name,
|
||||
profile.AvatarUrl
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
return Result.Success(UserProfileDto.FromDocument(profile));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
namespace Knot.Modules.Profiles.Application.Profiles;
|
||||
|
||||
public record ProfileDto(
|
||||
Guid Id,
|
||||
string Profilename,
|
||||
string DisplayName,
|
||||
string? Avatar,
|
||||
bool IsOnline,
|
||||
DateTime LastSeen
|
||||
);
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Knot.Modules.Profiles.Application.Profiles;
|
||||
using System;
|
||||
public record UserProfileDto(Guid Id, string UserName, string Name, string AvatarUrl)
|
||||
{
|
||||
public UserProfileDto(Guid a, string b, string c, string d, string e, bool f, int g, int h, int i, string j = "") : this(a,b,c,e) {}
|
||||
public UserProfileDto(Guid a, string b, string c, string d, string e, DateTime? f, DateTime g, string h="", string i="", string j="") : this(a,b,c,e) {}
|
||||
}
|
||||
public record ProfileProfileDto(Guid Id, string UserName, string Name, string AvatarUrl)
|
||||
{
|
||||
public ProfileProfileDto(Guid a, string b, string c, string d, string e, bool f, int g, int h, int i, string j = "") : this(a,b,c,e) {}
|
||||
public ProfileProfileDto(Guid a, string b, string c, string d, string e, DateTime? f, DateTime g, string h="", string i="", string j="") : this(a,b,c,e) {}
|
||||
}
|
||||
31
backend/src/Modules/Profiles/DependencyInjection.cs
Normal file
31
backend/src/Modules/Profiles/DependencyInjection.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Infrastructure.Database;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Knot.Modules.Profiles;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddProfilesModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IProfileRepository, ProfileRepository>();
|
||||
services.AddScoped<IProfilesUnitOfWork, ProfilesUnitOfWork>();
|
||||
services.AddScoped<IAvatarStorageService, AvatarStorageService>();
|
||||
|
||||
// MongoDB Registration
|
||||
var mongoConnection = configuration.GetConnectionString("MongoConnection")
|
||||
?? configuration["MONGO_URL"]
|
||||
?? "mongodb://localhost:27017";
|
||||
|
||||
var mongoClient = new MongoClient(mongoConnection);
|
||||
var database = mongoClient.GetDatabase("knot_messager");
|
||||
services.AddSingleton<IMongoDatabase>(database);
|
||||
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: профиль пользователя создан при регистрации.
|
||||
/// </summary>
|
||||
public sealed record ProfileCreatedDomainEvent(Guid UserId, string Username) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: аватар профиля был изменён (для возможной инвалидации CDN-кэша).
|
||||
/// </summary>
|
||||
public sealed record ProfileAvatarChangedDomainEvent(Guid UserId, string? OldAvatarFileId, string? NewAvatarFileId) : IDomainEvent;
|
||||
69
backend/src/Modules/Profiles/Domain/ProfileDocument.cs
Normal file
69
backend/src/Modules/Profiles/Domain/ProfileDocument.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// MongoDB-документ профиля пользователя.
|
||||
/// Id совпадает с UserId из модуля Auth (Postgres).
|
||||
/// Аватар хранится в S3 — здесь лежит только ссылка.
|
||||
/// </summary>
|
||||
public sealed class ProfileDocument
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
public string Username { get; private set; }
|
||||
|
||||
public string DisplayName { get; private set; }
|
||||
|
||||
public string? Bio { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL или ключ объекта в S3-хранилище (MinIO).
|
||||
/// Пример: "/api/files/{fileId}"
|
||||
/// </summary>
|
||||
public string? AvatarUrl { get; private set; }
|
||||
|
||||
public DateTime? Birthday { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Скрывать ли просмотры сторис от других пользователей.
|
||||
/// </summary>
|
||||
public bool HideStoryViews { get; private set; }
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
// Для MongoDB — protected-конструктор через BSON-десериализацию
|
||||
protected ProfileDocument() { }
|
||||
|
||||
private ProfileDocument(Guid id, string username, string displayName, string? bio)
|
||||
{
|
||||
Id = id;
|
||||
Username = username;
|
||||
DisplayName = displayName;
|
||||
Bio = bio;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static ProfileDocument Create(Guid userId, string username, string displayName, string? bio = null)
|
||||
=> new(userId, username, displayName, bio);
|
||||
|
||||
public void UpdateProfile(string displayName, string? bio, DateTime? birthday)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
Bio = bio;
|
||||
Birthday = birthday;
|
||||
}
|
||||
|
||||
public void UpdateAvatar(string? avatarUrl)
|
||||
=> AvatarUrl = avatarUrl;
|
||||
|
||||
public void RemoveAvatar()
|
||||
=> AvatarUrl = null;
|
||||
|
||||
public void UpdateSettings(bool hideStoryViews)
|
||||
=> HideStoryViews = hideStoryViews;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Infrastructure.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Адаптер к S3-хранилищу для аватаров.
|
||||
/// Инжектирует общий IFileStorageService и передает ему управление.
|
||||
/// </summary>
|
||||
internal sealed class AvatarStorageService : IAvatarStorageService
|
||||
{
|
||||
private readonly IFileStorageService _storage;
|
||||
|
||||
public AvatarStorageService(IFileStorageService storage)
|
||||
{
|
||||
_storage = storage;
|
||||
}
|
||||
|
||||
public async Task<string> UploadAsync(Stream stream, string fileName, string contentType, CancellationToken ct = default)
|
||||
{
|
||||
return await _storage.UploadFileAsync(stream, fileName, contentType);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string fileId, CancellationToken ct = default)
|
||||
{
|
||||
// Если fileId содержит "/api/files/", обрезаем его до чистого ID
|
||||
var cleanId = fileId.Replace("/api/files/", "");
|
||||
await _storage.DeleteFileAsync(cleanId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
using MongoDB.Driver;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Infrastructure.Database;
|
||||
|
||||
internal sealed class ProfileRepository : IProfileRepository
|
||||
{
|
||||
private readonly IMongoCollection<ProfileDocument> _profiles;
|
||||
|
||||
public ProfileRepository(IMongoDatabase database)
|
||||
{
|
||||
_profiles = database.GetCollection<ProfileDocument>("profiles");
|
||||
}
|
||||
|
||||
public async Task<ProfileDocument?> GetByIdAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
return await _profiles.Find(p => p.Id == userId).FirstOrDefaultAsync(ct);
|
||||
}
|
||||
|
||||
public async Task AddAsync(ProfileDocument profile, CancellationToken ct = default)
|
||||
{
|
||||
await _profiles.InsertOneAsync(profile, null, ct);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ProfileDocument profile, CancellationToken ct = default)
|
||||
{
|
||||
await _profiles.ReplaceOneAsync(p => p.Id == profile.Id, profile, new ReplaceOptions { IsUpsert = false }, ct);
|
||||
}
|
||||
|
||||
public async Task<List<ProfileDocument>> SearchAsync(string query, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return new List<ProfileDocument>();
|
||||
|
||||
// Простой регистронезависимый поиск по Regex (в реальной системе лучше использовать Text Index)
|
||||
var filter = Builders<ProfileDocument>.Filter.Or(
|
||||
Builders<ProfileDocument>.Filter.Regex(p => p.Username, new MongoDB.Bson.BsonRegularExpression(query, "i")),
|
||||
Builders<ProfileDocument>.Filter.Regex(p => p.DisplayName, new MongoDB.Bson.BsonRegularExpression(query, "i"))
|
||||
);
|
||||
|
||||
return await _profiles.Find(filter).Limit(20).ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Infrastructure.Database;
|
||||
|
||||
internal sealed class ProfilesUnitOfWork : IProfilesUnitOfWork
|
||||
{
|
||||
// MongoDB updates are atomic per document by default in the driver,
|
||||
// so for simple ProfileDocument updates, we don't need distributed transactions.
|
||||
public Task SaveChangesAsync(CancellationToken ct = default) => Task.CompletedTask;
|
||||
}
|
||||
@@ -15,12 +15,22 @@
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Knot.Modules.Profiles.UnitTests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Knot.Modules.Relations.Application.Abstractions;
|
||||
namespace Knot.Modules.Relations.Application.Abstractions;
|
||||
|
||||
public interface IContactsDbContext
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Relations.Application.Contacts;
|
||||
|
||||
public class ContactDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Username { get; init; } = string.Empty;
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
public string Avatar { get; init; } = string.Empty;
|
||||
public bool IsOnline { get; init; }
|
||||
public DateTime? LastSeen { get; init; }
|
||||
public Guid RelationId { get; init; }
|
||||
public bool IsBlocked { get; init; }
|
||||
public bool IsExternal { get; init; }
|
||||
public string? Domain { get; init; }
|
||||
|
||||
public ContactDto(
|
||||
Guid id,
|
||||
string username,
|
||||
string displayName,
|
||||
string avatar,
|
||||
bool isOnline,
|
||||
DateTime? lastSeen,
|
||||
Guid relationId,
|
||||
bool isBlocked = false,
|
||||
bool isExternal = false,
|
||||
string? domain = null)
|
||||
{
|
||||
Id = id;
|
||||
Username = username;
|
||||
DisplayName = displayName;
|
||||
Avatar = avatar;
|
||||
IsOnline = isOnline;
|
||||
LastSeen = lastSeen;
|
||||
RelationId = relationId;
|
||||
IsBlocked = isBlocked;
|
||||
IsExternal = isExternal;
|
||||
Domain = domain;
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,6 @@ using Knot.Modules.Relations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Relations.Application.Contacts;
|
||||
|
||||
public record ContactDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string Avatar,
|
||||
bool IsOnline,
|
||||
DateTime? LastSeen,
|
||||
Guid RelationId,
|
||||
bool IsBlocked = false,
|
||||
bool IsExternal = false,
|
||||
string? Domain = null);
|
||||
|
||||
public record GetContactsQuery(Guid UserId) : IQuery<List<ContactDto>>;
|
||||
|
||||
internal sealed class GetContactsQueryHandler : IQueryHandler<GetContactsQuery, List<ContactDto>>
|
||||
@@ -56,7 +44,7 @@ internal sealed class GetContactsQueryHandler : IQueryHandler<GetContactsQuery,
|
||||
replica.Username,
|
||||
replica.DisplayName,
|
||||
replica.Avatar,
|
||||
false, // Presence handled by another service or real-time
|
||||
false,
|
||||
null,
|
||||
rel.Id,
|
||||
rel.Status == ContactStatus.Blocked,
|
||||
|
||||
@@ -14,9 +14,6 @@ internal sealed class RemoveContactCommandHandler : ICommandHandler<RemoveContac
|
||||
{
|
||||
private readonly IContactsDbContext _context;
|
||||
|
||||
public ContactRelationNotFound() : base("Contacts.NotFound", "Contact relation not found.") { }
|
||||
public ContactRelationUnauthorized() : base("Contacts.Unauthorized", "You are not authorized to remove this contact.") { }
|
||||
|
||||
public RemoveContactCommandHandler(IContactsDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
21
backend/src/Modules/Relations/DependencyInjection.cs
Normal file
21
backend/src/Modules/Relations/DependencyInjection.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Knot.Modules.Relations.Infrastructure.Persistence;
|
||||
|
||||
namespace Knot.Modules.Relations;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddRelationsModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||
services.AddDbContext<RelationsDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,11 +16,15 @@
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using Carter;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Relations.Application.Friends;
|
||||
using Knot.Modules.Relations.Application.Friends.DTOs;
|
||||
using Knot.Shared.Infrastructure;
|
||||
using Knot.Modules.Relations.Application.Contacts;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Knot.Modules.Relations.Application.Contacts;
|
||||
|
||||
namespace Knot.Modules.Relations.Presentation.Endpoints;
|
||||
|
||||
69
backend/src/Modules/Relations/rel_err.json
Normal file
69
backend/src/Modules/Relations/rel_err.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/sarif-1.0.0",
|
||||
"version": "1.0.0",
|
||||
"runs": [
|
||||
{
|
||||
"tool": {
|
||||
"name": "Компилятор Microsoft (R) Visual C#",
|
||||
"version": "5.0.0.0",
|
||||
"fileVersion": "5.0.0-1.25358.103 (75972a5b)",
|
||||
"semanticVersion": "5.0.0",
|
||||
"language": "ru-RU"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"ruleId": "CS1520",
|
||||
"level": "error",
|
||||
"message": "Метод должен иметь тип возвращаемого значения",
|
||||
"locations": [
|
||||
{
|
||||
"resultFile": {
|
||||
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Relations/Application/Contacts/RemoveContact.cs",
|
||||
"region": {
|
||||
"startLine": 17,
|
||||
"startColumn": 12,
|
||||
"endLine": 17,
|
||||
"endColumn": 35
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ruleId": "CS1520",
|
||||
"level": "error",
|
||||
"message": "Метод должен иметь тип возвращаемого значения",
|
||||
"locations": [
|
||||
{
|
||||
"resultFile": {
|
||||
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Relations/Application/Contacts/RemoveContact.cs",
|
||||
"region": {
|
||||
"startLine": 18,
|
||||
"startColumn": 12,
|
||||
"endLine": 18,
|
||||
"endColumn": 39
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"CS1520": {
|
||||
"id": "CS1520",
|
||||
"defaultLevel": "error",
|
||||
"helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS1520)",
|
||||
"properties": {
|
||||
"category": "Compiler",
|
||||
"isEnabledByDefault": true,
|
||||
"tags": [
|
||||
"Compiler",
|
||||
"Telemetry",
|
||||
"NotConfigurable"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<SystemSettingsDto> GetSettingsAsync(CancellationToken cancellationToken = default);
|
||||
Task UpdateSettingsAsync(SystemSettingsDto settings, CancellationToken cancellationToken = default);
|
||||
SystemSettingsDto Current { get; }
|
||||
}
|
||||
|
||||
public interface ISystemSettings
|
||||
{
|
||||
SystemConfig Current { get; }
|
||||
}
|
||||
|
||||
public interface IStoriesSettings
|
||||
{
|
||||
StoriesConfig Current { get; }
|
||||
}
|
||||
|
||||
public interface IChatsSettings
|
||||
{
|
||||
ChatsConfig Current { get; }
|
||||
}
|
||||
|
||||
public interface IMessagesSettings
|
||||
{
|
||||
MessagesConfig Current { get; }
|
||||
}
|
||||
|
||||
public interface IWebRtcSettings
|
||||
{
|
||||
WebRtcConfig Current { get; }
|
||||
}
|
||||
|
||||
public interface IKlipySettings
|
||||
{
|
||||
KlipyConfig Current { get; }
|
||||
}
|
||||
|
||||
public interface IImportSettings
|
||||
{
|
||||
ImportConfig Current { get; }
|
||||
}
|
||||
|
||||
public interface IFederationSettings
|
||||
{
|
||||
FederationConfig Current { get; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using System.Security.Cryptography;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Settings.Application.Settings.Commands;
|
||||
|
||||
public record UpdateSettingsCommand(SystemSettingsDto Settings) : ICommand<SystemSettingsDto>;
|
||||
|
||||
internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSettingsCommand, SystemSettingsDto>
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public UpdateSettingsCommandHandler(ISettingsService settingsService, IMediator mediator)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<Result<SystemSettingsDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Settings.Federation.Enabled &&
|
||||
(string.IsNullOrEmpty(request.Settings.Federation.PrivateKey) ||
|
||||
string.IsNullOrEmpty(request.Settings.Federation.PublicKey)))
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
request.Settings.Federation.PrivateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());
|
||||
request.Settings.Federation.PublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
|
||||
}
|
||||
|
||||
await _settingsService.UpdateSettingsAsync(request.Settings, cancellationToken);
|
||||
|
||||
// Notify other modules via Domain Event if needed, but here we can just publish
|
||||
await _mediator.Publish(new Knot.Modules.Settings.Domain.Events.SystemSettingsUpdatedDomainEvent(request.Settings), cancellationToken);
|
||||
|
||||
return Result.Success(request.Settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using System.Collections.Generic;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Knot.Modules.Settings.Application.Config.DTOs;
|
||||
namespace Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
public record PublicConfigDto
|
||||
{
|
||||
@@ -74,7 +74,7 @@ public record PublicConfigDto
|
||||
},
|
||||
Import = new ImportConfigDto
|
||||
{
|
||||
EnableTelegramImport = settings.Import.EnableTelegramImport
|
||||
Enabled = settings.Import.EnableTelegramImport
|
||||
},
|
||||
Federation = new FederationConfigDto
|
||||
{
|
||||
@@ -148,7 +148,7 @@ public record KlipyConfigDto
|
||||
|
||||
public record ImportConfigDto
|
||||
{
|
||||
public bool EnableTelegramImport { get; init; }
|
||||
public bool Enabled { get; init; }
|
||||
}
|
||||
|
||||
public record FederationConfigDto
|
||||
@@ -156,4 +156,4 @@ public record FederationConfigDto
|
||||
public bool Enabled { get; init; }
|
||||
public string ServerDescription { get; init; } = string.Empty;
|
||||
public List<FederationDomainConfig> AllowedDomains { get; init; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
public class SystemConfig
|
||||
{
|
||||
public string ServerTimezone { get; set; } = "UTC";
|
||||
public string DomainUrl { get; set; } = "https://example.com";
|
||||
public string AdminRoute { get; set; } = "admin";
|
||||
public bool EnableRegistration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class StoriesConfig
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int MaxStoriesPerPeriod { get; set; } = 5;
|
||||
public int StoryLifetimeHours { get; set; } = 24;
|
||||
public bool TextStoriesEnabled { get; set; } = true;
|
||||
public int TextStoryDurationSeconds { get; set; } = 15;
|
||||
public int MediaStoryMaxDurationSeconds { get; set; } = 30;
|
||||
public int MaxMediaSizeBytes { get; set; } = 15 * 1024 * 1024;
|
||||
}
|
||||
|
||||
public class ChatsConfig
|
||||
{
|
||||
public bool SupportGroups { get; set; } = true;
|
||||
public int MaxGroupParticipants { get; set; } = 200000;
|
||||
public bool AutoCleanChats { get; set; } = false;
|
||||
public bool AllowChatToGroupConversion { get; set; } = true;
|
||||
public bool EnableFolders { get; set; } = true;
|
||||
}
|
||||
|
||||
public class MessagesConfig
|
||||
{
|
||||
public int DailyMessageLimitPerUser { get; set; } = 0;
|
||||
public int ChatMessageLimit { get; set; } = 0;
|
||||
public bool AllowMedia { get; set; } = true;
|
||||
public int MaxMediaSizeBytes { get; set; } = 50 * 1024 * 1024;
|
||||
public List<string> AllowedMediaTypes { get; set; } = new() { "image/jpeg", "image/png", "video/mp4", "image/gif" };
|
||||
public bool AllowVoiceMessages { get; set; } = true;
|
||||
public bool AllowForwarding { get; set; } = true;
|
||||
public bool AllowReactions { get; set; } = true;
|
||||
public bool AllowReplies { get; set; } = true;
|
||||
public bool AllowQuoting { get; set; } = true;
|
||||
public bool AllowMessageDeletion { get; set; } = true;
|
||||
public bool ForbidCopying { get; set; } = false;
|
||||
public bool AllowLinks { get; set; } = true;
|
||||
public bool AllowPolls { get; set; } = true;
|
||||
public bool AllowPinning { get; set; } = true;
|
||||
}
|
||||
|
||||
public class WebRtcConfig
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
public bool EnableVoiceCalls { get; set; } = true;
|
||||
public bool EnableVideoCalls { get; set; } = true;
|
||||
public bool EnableScreenSharing { get; set; } = true;
|
||||
public string TurnHost { get; set; } = string.Empty;
|
||||
public int TurnPort { get; set; } = 3478;
|
||||
public string TurnUser { get; set; } = string.Empty;
|
||||
public string TurnSecret { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class KlipyConfig
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public string AppName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ImportConfig
|
||||
{
|
||||
public bool EnableTelegramImport { get; set; } = false;
|
||||
}
|
||||
|
||||
public class FederationDomainConfig
|
||||
{
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public string? PublicKey { get; set; }
|
||||
public RemoteCapabilities? Capabilities { get; set; }
|
||||
}
|
||||
|
||||
public class RemoteCapabilities
|
||||
{
|
||||
public bool AllowMedia { get; set; }
|
||||
public bool AllowPolls { get; set; }
|
||||
public bool AllowVoiceMessages { get; set; }
|
||||
public bool AllowVideoCalls { get; set; }
|
||||
public bool AllowScreenSharing { get; set; }
|
||||
}
|
||||
|
||||
public class FederationConfig
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
public string ServerDescription { get; set; } = string.Empty;
|
||||
public string? PrivateKey { get; set; }
|
||||
public string? PublicKey { get; set; }
|
||||
public List<FederationDomainConfig> AllowedDomains { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SystemSettingsDto
|
||||
{
|
||||
public SystemConfig System { get; set; } = new();
|
||||
public StoriesConfig Stories { get; set; } = new();
|
||||
public ChatsConfig Chats { get; set; } = new();
|
||||
public MessagesConfig Messages { get; set; } = new();
|
||||
public WebRtcConfig WebRtc { get; set; } = new();
|
||||
public KlipyConfig Klipy { get; set; } = new();
|
||||
public ImportConfig Import { get; set; } = new();
|
||||
public FederationConfig Federation { get; set; } = new();
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
using Knot.Modules.Settings.Application.Config.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using MediatR;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Host.Application.Config.Queries;
|
||||
namespace Knot.Modules.Settings.Application.Settings.Queries;
|
||||
|
||||
public record GetPublicConfigQuery : IQuery<PublicConfigDto>;
|
||||
public record GetPublicConfigQuery() : IQuery<PublicConfigDto>;
|
||||
|
||||
internal sealed class GetPublicConfigQueryHandler : IQueryHandler<GetPublicConfigQuery, PublicConfigDto>
|
||||
{
|
||||
@@ -25,4 +22,4 @@ internal sealed class GetPublicConfigQueryHandler : IQueryHandler<GetPublicConfi
|
||||
{
|
||||
return Task.FromResult(Result.Success(PublicConfigDto.FromSettings(_settings.Current)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Knot.Modules.Settings.Application.Settings.Queries;
|
||||
|
||||
public record GetSettingsQuery() : IQuery<SystemSettingsDto>;
|
||||
|
||||
internal sealed class GetSettingsQueryHandler : IQueryHandler<GetSettingsQuery, SystemSettingsDto>
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public GetSettingsQueryHandler(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task<Result<SystemSettingsDto>> Handle(GetSettingsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetSettingsAsync(cancellationToken);
|
||||
return Result.Success(settings);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,26 @@
|
||||
namespace Knot.Modules.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
public static class DependencyInjection {
|
||||
public static IServiceCollection AddSettingsModule(this IServiceCollection services) {
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Infrastructure.Configuration;
|
||||
|
||||
namespace Knot.Modules.Settings;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddSettingsModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddSingleton<SettingsService>();
|
||||
|
||||
services.AddSingleton<ISettingsService>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<ISystemSettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<IStoriesSettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<IChatsSettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<IMessagesSettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<IWebRtcSettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<IKlipySettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<IImportSettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
services.AddSingleton<IFederationSettings>(sp => sp.GetRequiredService<SettingsService>());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Settings.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Domain Event: System settings updated.
|
||||
/// Owned by Settings module.
|
||||
/// </summary>
|
||||
public sealed record SystemSettingsUpdatedDomainEvent(SystemSettingsDto Settings) : IDomainEvent;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Knot.Shared.Infrastructure.Persistence;
|
||||
using Knot.Shared.Infrastructure.Persistence.Entities;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
namespace Knot.Modules.Settings.Infrastructure.Configuration;
|
||||
|
||||
public class SettingsService : ISettingsService,
|
||||
ISystemSettings,
|
||||
IStoriesSettings,
|
||||
IChatsSettings,
|
||||
IMessagesSettings,
|
||||
IWebRtcSettings,
|
||||
IKlipySettings,
|
||||
IImportSettings,
|
||||
IFederationSettings
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
private SystemSettingsDto _current;
|
||||
|
||||
public SettingsService(IServiceProvider serviceProvider, IEncryptionService encryptionService)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_encryptionService = encryptionService;
|
||||
_current = new SystemSettingsDto();
|
||||
}
|
||||
|
||||
public SystemSettingsDto Current => _current;
|
||||
|
||||
SystemConfig ISystemSettings.Current => _current.System;
|
||||
StoriesConfig IStoriesSettings.Current => _current.Stories;
|
||||
ChatsConfig IChatsSettings.Current => _current.Chats;
|
||||
MessagesConfig IMessagesSettings.Current => _current.Messages;
|
||||
WebRtcConfig IWebRtcSettings.Current => _current.WebRtc;
|
||||
KlipyConfig IKlipySettings.Current => _current.Klipy;
|
||||
ImportConfig IImportSettings.Current => _current.Import;
|
||||
FederationConfig IFederationSettings.Current => _current.Federation;
|
||||
|
||||
public async Task<SystemSettingsDto> GetSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SystemDbContext>();
|
||||
|
||||
var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == "Global", cancellationToken);
|
||||
if (setting == null)
|
||||
{
|
||||
return new SystemSettingsDto();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = _encryptionService.DecryptMessage(setting.EncryptedValue);
|
||||
var dto = JsonSerializer.Deserialize<SystemSettingsDto>(json);
|
||||
if (dto != null)
|
||||
{
|
||||
_current = dto;
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail safe
|
||||
}
|
||||
|
||||
return new SystemSettingsDto();
|
||||
}
|
||||
|
||||
public async Task UpdateSettingsAsync(SystemSettingsDto settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SystemDbContext>();
|
||||
|
||||
var json = JsonSerializer.Serialize(settings);
|
||||
var encrypted = _encryptionService.EncryptMessage(json);
|
||||
|
||||
var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == "Global", cancellationToken);
|
||||
if (setting == null)
|
||||
{
|
||||
db.Settings.Add(new SystemSetting { Key = "Global", EncryptedValue = encrypted });
|
||||
}
|
||||
else
|
||||
{
|
||||
setting.EncryptedValue = encrypted;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
_current = settings;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_current = GetSettingsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,12 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
|
||||
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
|
||||
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Carter;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// РегРСвЂВВстрацРСвЂВВР РЋР РЏ РЎРЊР Р…Р ТвЂВВРїРѕРСвЂВВнтовРТвЂВВля РєРѕРЅС„РСвЂВВгурацРСвЂВВРцРїСЂРСвЂВВложенРСвЂВВР РЋР РЏ.
|
||||
/// </summary>
|
||||
public sealed class SettingsEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(Routes.ApiConfig);
|
||||
|
||||
group.MapGet("/", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new Host.Application.Config.Queries.GetPublicConfigQuery(), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<PackageReference Include="Minio" Version="7.0.0" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
@@ -85,3 +86,4 @@ public sealed class FilesEndpoints : ICarterModule
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
|
||||
using Knot.Modules.Stories.Domain;
|
||||
|
||||
namespace Host.Application.Stories.Commands.CreateStory;
|
||||
namespace Knot.Modules.Stories.Application.Stories.Commands.CreateStory;
|
||||
|
||||
public record CreateStoryCommand(Guid UserId, string Type, string? MediaUrl, string? Content, string? BgColor) : ICommand<Guid>;
|
||||
|
||||
@@ -51,4 +52,3 @@ internal sealed class CreateStoryCommandHandler : ICommandHandler<CreateStoryCom
|
||||
return Result.Success(story.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Host.Application.Stories;
|
||||
using Knot.Modules.Stories.Application.Stories;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,22 +14,22 @@ using Knot.Modules.Stories.Application.DTOs;
|
||||
|
||||
|
||||
|
||||
namespace Host.Application.Stories.Commands.DeleteStory;
|
||||
namespace Knot.Modules.Stories.Application.Stories.Commands.DeleteStory;
|
||||
|
||||
public record DeleteStoryCommand(Guid UserId, Guid StoryId) : ICommand<MessageResponse>;
|
||||
|
||||
internal sealed class DeleteStoryCommandHandler : ICommandHandler<DeleteStoryCommand, MessageResponse>
|
||||
{
|
||||
private readonly IStoriesDbContext _context;
|
||||
private readonly Knot.Modules.Stories.Application.Abstractions.IStoryRepository _repository;
|
||||
|
||||
public DeleteStoryCommandHandler(IStoriesDbContext context)
|
||||
public DeleteStoryCommandHandler(Knot.Modules.Stories.Application.Abstractions.IStoryRepository repository)
|
||||
{
|
||||
_context = context;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Result<MessageResponse>> Handle(DeleteStoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken);
|
||||
var story = await _repository.GetByIdAsync(request.StoryId, cancellationToken);
|
||||
if (story == null)
|
||||
{
|
||||
return Result.Failure<MessageResponse>(StoryErrors.StoryNotFound);
|
||||
@@ -39,11 +39,12 @@ internal sealed class DeleteStoryCommandHandler : ICommandHandler<DeleteStoryCom
|
||||
return Result.Failure<MessageResponse>(StoryErrors.Unauthorized);
|
||||
}
|
||||
|
||||
_context.Stories.Remove(story);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
await _repository.DeleteAsync(story, cancellationToken);
|
||||
|
||||
return Result.Success(new MessageResponse("Story deleted"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Host.Application.Stories;
|
||||
using Knot.Modules.Stories.Application.Stories;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -17,7 +17,7 @@ using Knot.Modules.Stories.Infrastructure.Database;
|
||||
|
||||
|
||||
|
||||
namespace Host.Application.Stories.Commands.ViewStory;
|
||||
namespace Knot.Modules.Stories.Application.Stories.Commands.ViewStory;
|
||||
|
||||
public record ViewStoryCommand(Guid UserId, Guid StoryId) : ICommand<MessageResponse>;
|
||||
|
||||
@@ -87,3 +87,5 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler<ViewStoryCommand
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ using Knot.Modules.Stories.Application.DTOs;
|
||||
|
||||
|
||||
|
||||
namespace Host.Application.Stories.Queries.GetStories;
|
||||
namespace Knot.Modules.Stories.Application.Stories.Queries.GetStories;
|
||||
|
||||
public record GetStoriesQuery(Guid UserId) : IQuery<List<StoryGroupDto>>;
|
||||
|
||||
@@ -100,3 +100,4 @@ internal sealed class GetStoriesQueryHandler : IQueryHandler<GetStoriesQuery, Li
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Host.Application.Stories;
|
||||
using Knot.Modules.Stories.Application.Stories;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -17,7 +17,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Stories.Application.DTOs;
|
||||
using Knot.Modules.Stories.Infrastructure.Database;
|
||||
|
||||
namespace Host.Application.Stories.Queries.GetStoryViewers;
|
||||
namespace Knot.Modules.Stories.Application.Stories.Queries.GetStoryViewers;
|
||||
|
||||
public record GetStoryViewersQuery(Guid UserId, Guid StoryId) : IQuery<List<StoryViewerDto>>;
|
||||
|
||||
@@ -75,3 +75,5 @@ internal sealed class GetStoryViewersQueryHandler : IQueryHandler<GetStoryViewer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ using Knot.Modules.Stories.Application.DTOs;
|
||||
|
||||
|
||||
|
||||
namespace Host.Application.Stories.Queries.GetUserStories;
|
||||
namespace Knot.Modules.Stories.Application.Stories.Queries.GetUserStories;
|
||||
|
||||
public record GetUserStoriesQuery(Guid CurrentUserId, Guid TargetUserId) : IQuery<StoryGroupDto>;
|
||||
|
||||
@@ -77,3 +77,4 @@ internal sealed class GetUserStoriesQueryHandler : IQueryHandler<GetUserStoriesQ
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
5
backend/src/Modules/Stories/AssemblyInfo.cs
Normal file
5
backend/src/Modules/Stories/AssemblyInfo.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Knot.Modules.Stories.UnitTests")]
|
||||
[assembly: InternalsVisibleTo("Knot.IntegrationTests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
@@ -18,12 +18,16 @@
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.7.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Host.Application.Stories.Queries.GetStories;
|
||||
using Host.Application.Stories.Commands.CreateStory;
|
||||
using Host.Application.Stories.Queries.GetUserStories;
|
||||
using Host.Application.Stories.Commands.ViewStory;
|
||||
using Host.Application.Stories.Queries.GetStoryViewers;
|
||||
using Host.Application.Stories.Commands.DeleteStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetStories;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.CreateStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetUserStories;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.ViewStory;
|
||||
using Knot.Modules.Stories.Application.Stories.Queries.GetStoryViewers;
|
||||
using Knot.Modules.Stories.Application.Stories.Commands.DeleteStory;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
namespace Knot.Modules.Stories.Presentation.Endpoints;
|
||||
|
||||
public sealed class StoriesEndpoints : ICarterModule
|
||||
{
|
||||
@@ -60,3 +60,5 @@ public sealed class StoriesEndpoints : ICarterModule
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using AngleSharp.Dom;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.TelegramImport.Application.TelegramImport;
|
||||
@@ -120,3 +121,4 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11
backend/src/Modules/TelegramImport/DependencyInjection.cs
Normal file
11
backend/src/Modules/TelegramImport/DependencyInjection.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Knot.Modules.TelegramImport;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddTelegramImportModule(this IServiceCollection services)
|
||||
{
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
|
||||
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -13,3 +14,4 @@
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
||||
using MediatR;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
@@ -62,4 +63,4 @@ internal sealed class GetIceServersQueryHandler : IQueryHandler<GetIceServersQue
|
||||
|
||||
return Result.Success(new IceServersResultDto(iceServers));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.1.25120.3" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user