From 8025340e4530441c217269f35bf74a8b3543be4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Tue, 31 Mar 2026 01:54:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Abstractions/IStatisticsService.cs | 37 ------- .../GetDashboardStatsQuery.cs | 2 +- .../GetUserDetails/GetUserDetailsQuery.cs | 8 +- .../Queries/SearchUsers/SearchUsersQuery.cs | 6 +- .../Presentation/Endpoints/AdminEndpoints.cs | 21 +++- ...8000000_AddUserUserDomainField.Designer.cs | 104 ++++++++++++++++++ .../20270328000000_AddUserUserDomainField.cs | 31 ++++++ .../Conversations/DependencyInjection.cs | 1 + .../Services/UserStatusService.cs | 2 +- .../Modules/Messaging/DependencyInjection.cs | 3 +- .../Adapters/UserStatsServiceAdapter.cs | 24 ++++ docker/nginx/default.conf | 12 +- 12 files changed, 189 insertions(+), 62 deletions(-) delete mode 100644 backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs create mode 100644 backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.Designer.cs create mode 100644 backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.cs create mode 100644 backend/src/Modules/Messaging/Infrastructure/Adapters/UserStatsServiceAdapter.cs diff --git a/backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs b/backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs deleted file mode 100644 index da97d46..0000000 --- a/backend/src/Contracts/Settings/Abstractions/IStatisticsService.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Knot.Contracts.Settings.Abstractions; - -public interface IStatisticsService -{ - Task GetDashboardStatsAsync(CancellationToken cancellationToken = default); -} - -public class DashboardStatsDto -{ - public long StorageUsedBytes { get; set; } - public long StorageLimitBytes { get; set; } - public long OnlineUsers { get; set; } - public long OfflineUsers { get; set; } - public long TotalUsers { get; set; } - public List ActivityTimeline { get; set; } = new(); - public List TopUsersByMessages { get; set; } = new(); - public List TopUsersByStorage { get; set; } = new(); -} - -public class ActivityStatDto -{ - public DateTime Date { get; set; } - public long Messages { get; set; } - public long FilesSize { get; set; } -} - -public class TopUserDto -{ - public Guid UserId { get; set; } - public string Username { get; set; } = string.Empty; - public long Value { get; set; } // messages count or bytes -} diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs index 009ec59..25d36bb 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs @@ -1,8 +1,8 @@ using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Admin.Abstractions; -using Knot.Contracts.Settings.Abstractions; using Knot.Modules.Admin.Application.Admin.DTOs; +using Knot.Shared.Kernel.Services; using MediatR; namespace Knot.Modules.Admin.Application.Admin.Queries; diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs index bc2ede1..3e3fbfe 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Admin.Abstractions; using Knot.Contracts.Auth.Domain; -using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Abstractions; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Modules.Admin.Application.Admin.DTOs; using MediatR; @@ -18,12 +16,12 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler + group.MapGet("users", async (ISender sender, [FromQuery] string? query, CancellationToken ct = default) => { - var result = await sender.Send(new SearchUsersQuery(query), ct); - if (!result.IsSuccess) + // При пустом или отсутствующем query возвращаем всех пользователей + var searchQuery = string.IsNullOrWhiteSpace(query) ? "" : query; + try { - return Results.BadRequest(new { error = result.Error.Description }); + var result = await sender.Send(new SearchUsersQuery(searchQuery), ct); + if (!result.IsSuccess) + { + Console.WriteLine($"[Admin] SearchUsers error: {result.Error.Code} - {result.Error.Description}"); + return Results.BadRequest(new { error = result.Error.Description }); + } + return Results.Ok(result.Value); + } + catch (Exception ex) + { + Console.WriteLine($"[Admin] SearchUsers exception: {ex}"); + return Results.BadRequest(new { error = ex.Message }); } - return Results.Ok(result.Value); }); group.MapGet("users/{id:guid}", async ([FromRoute] Guid id, ISender sender, CancellationToken ct) => diff --git a/backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.Designer.cs b/backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.Designer.cs new file mode 100644 index 0000000..f1d0758 --- /dev/null +++ b/backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.Designer.cs @@ -0,0 +1,104 @@ +// +using System; +using Knot.Modules.Auth.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Knot.Modules.Auth.Migrations +{ + [DbContext(typeof(AuthDbContext))] + [Migration("20270328000000_AddUserUserDomainField")] + partial class AddUserUserDomainField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("identity") + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BannedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Domain") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("HideStatus") + .HasColumnType("boolean"); + + b.Property("HideStoryViews") + .HasColumnType("boolean"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsOnline") + .HasColumnType("boolean"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("RefreshToken") + .HasColumnType("text"); + + b.Property("UserDomain") + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "identity"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.cs b/backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.cs new file mode 100644 index 0000000..5d5f560 --- /dev/null +++ b/backend/src/Modules/Auth/Migrations/20270328000000_AddUserUserDomainField.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Knot.Modules.Auth.Migrations +{ + /// + public partial class AddUserUserDomainField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UserDomain", + schema: "identity", + table: "Users", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UserDomain", + schema: "identity", + table: "Users"); + } + } +} diff --git a/backend/src/Modules/Conversations/DependencyInjection.cs b/backend/src/Modules/Conversations/DependencyInjection.cs index bcd56bc..55b264d 100644 --- a/backend/src/Modules/Conversations/DependencyInjection.cs +++ b/backend/src/Modules/Conversations/DependencyInjection.cs @@ -42,6 +42,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs b/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs index aa59627..ce6ecd4 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs @@ -3,7 +3,7 @@ using Knot.Modules.Conversations.Infrastructure.SignalR; namespace Knot.Modules.Conversations.Infrastructure.Services; -public sealed class UserStatusService : IUserStatusService +public sealed class UserStatusService : IUserStatusService, Knot.Contracts.Conversations.Abstractions.IUserStatusService { public bool IsUserOnline(string userId) { diff --git a/backend/src/Modules/Messaging/DependencyInjection.cs b/backend/src/Modules/Messaging/DependencyInjection.cs index 0ed12df..c869799 100644 --- a/backend/src/Modules/Messaging/DependencyInjection.cs +++ b/backend/src/Modules/Messaging/DependencyInjection.cs @@ -1,6 +1,7 @@ using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Modules.Messaging.Application.Abstractions; +using Knot.Modules.Messaging.Infrastructure.Adapters; using Knot.Modules.Messaging.Infrastructure.Persistence; using Knot.Modules.Messaging.Infrastructure.Persistence.Mongo; using Knot.Shared.Kernel; @@ -30,7 +31,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(sp => (ContractUserStatsService)sp.GetRequiredService()); + services.AddScoped(); services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); diff --git a/backend/src/Modules/Messaging/Infrastructure/Adapters/UserStatsServiceAdapter.cs b/backend/src/Modules/Messaging/Infrastructure/Adapters/UserStatsServiceAdapter.cs new file mode 100644 index 0000000..92a8c86 --- /dev/null +++ b/backend/src/Modules/Messaging/Infrastructure/Adapters/UserStatsServiceAdapter.cs @@ -0,0 +1,24 @@ +using Knot.Contracts.Messaging.Application.Abstractions; +using ModuleUserStats = Knot.Modules.Messaging.Application.Abstractions.UserStats; +using ModuleUserStatsService = Knot.Modules.Messaging.Application.Abstractions.IUserStatsService; + +namespace Knot.Modules.Messaging.Infrastructure.Adapters; + +internal sealed class UserStatsServiceAdapter : IUserStatsService +{ + private readonly ModuleUserStatsService _inner; + + public UserStatsServiceAdapter(ModuleUserStatsService inner) + { + _inner = inner; + } + + public async Task> GetStatsForUsersAsync(List userIds, CancellationToken cancellationToken = default) + { + var moduleStats = await _inner.GetStatsForUsersAsync(userIds, cancellationToken); + return moduleStats.ToDictionary( + kvp => kvp.Key, + kvp => new UserStats(kvp.Value.MessageCount, kvp.Value.StorageSize) + ); + } +} diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 5a6f0b2..ea73aee 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -11,9 +11,7 @@ server { # Proxy API requests location /api/ { - resolver 127.0.0.11 valid=30s; - set $backend_server server; - proxy_pass http://$backend_server:8080; + proxy_pass http://server:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -26,9 +24,7 @@ server { # Proxy SignalR hubs location /hubs/ { - resolver 127.0.0.11 valid=30s; - set $backend_server server; - proxy_pass http://$backend_server:8080; + proxy_pass http://server:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; @@ -39,9 +35,7 @@ server { # Static uploads location /uploads/ { - resolver 127.0.0.11 valid=30s; - set $backend_server server; - proxy_pass http://$backend_server:8080; + proxy_pass http://server:8080; proxy_set_header Host $host; } }