Админка

This commit is contained in:
Халимов Рустам
2026-03-31 01:54:35 +03:00
parent 60d3cf12ae
commit 8025340e45
12 changed files with 189 additions and 62 deletions

View File

@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Settings.Abstractions;
public interface IStatisticsService
{
Task<DashboardStatsDto> GetDashboardStatsAsync(CancellationToken cancellationToken = default);
}
public class DashboardStatsDto
{
public long StorageUsedBytes { get; set; }
public long StorageLimitBytes { get; set; }
public long OnlineUsers { get; set; }
public long OfflineUsers { get; set; }
public long TotalUsers { get; set; }
public List<ActivityStatDto> ActivityTimeline { get; set; } = new();
public List<TopUserDto> TopUsersByMessages { get; set; } = new();
public List<TopUserDto> TopUsersByStorage { get; set; } = new();
}
public class ActivityStatDto
{
public DateTime Date { get; set; }
public long Messages { get; set; }
public long FilesSize { get; set; }
}
public class TopUserDto
{
public Guid UserId { get; set; }
public string Username { get; set; } = string.Empty;
public long Value { get; set; } // messages count or bytes
}

View File

@@ -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;

View File

@@ -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<GetUserDetailsQ
{
private readonly IUserRepository _userRepository;
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService;
private readonly IUserStatusService _statusService;
public GetUserDetailsQueryHandler(
IUserRepository userRepository,
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService)
IUserStatusService statusService)
{
_userRepository = userRepository;
_statsService = statsService;

View File

@@ -5,7 +5,7 @@ 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 +18,12 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery,
{
private readonly IUserRepository _userRepository;
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService;
private readonly IUserStatusService _statusService;
public SearchUsersQueryHandler(
IUserRepository userRepository,
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService)
IUserStatusService statusService)
{
_userRepository = userRepository;
_statsService = statsService;

View File

@@ -67,14 +67,25 @@ public static class AdminEndpoints
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
});
group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) =>
group.MapGet("users", async (ISender sender, [FromQuery] string? query, CancellationToken ct = default) =>
{
var result = await sender.Send(new SearchUsersQuery(query), ct);
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) =>

View File

@@ -0,0 +1,104 @@
// <auto-generated />
using System;
using Knot.Modules.Auth.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Knot.Modules.Auth.Migrations
{
[DbContext(typeof(AuthDbContext))]
[Migration("20270328000000_AddUserUserDomainField")]
partial class AddUserUserDomainField
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("identity")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime?>("BannedUntil")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Birthday")
.HasColumnType("timestamp with time zone");
b.Property<string>("Bio")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Domain")
.HasColumnType("text");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<bool>("HideStatus")
.HasColumnType("boolean");
b.Property<bool>("HideStoryViews")
.HasColumnType("boolean");
b.Property<bool>("IsBanned")
.HasColumnType("boolean");
b.Property<bool>("IsExternal")
.HasColumnType("boolean");
b.Property<bool>("IsOnline")
.HasColumnType("boolean");
b.Property<DateTime?>("LastSeen")
.HasColumnType("timestamp with time zone");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<string>("RefreshToken")
.HasColumnType("text");
b.Property<string>("UserDomain")
.HasColumnType("text");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users", "identity");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Knot.Modules.Auth.Migrations
{
/// <inheritdoc />
public partial class AddUserUserDomainField : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "UserDomain",
schema: "identity",
table: "Users",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "UserDomain",
schema: "identity",
table: "Users");
}
}
}

View File

@@ -42,6 +42,7 @@ public static class DependencyInjection
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IChatAccessProvider, Knot.Modules.Conversations.Infrastructure.Services.ChatAccessProvider>();
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
services.AddScoped<ConversationsAbstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
return services;
}
}

View File

@@ -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)
{

View File

@@ -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<IMessageRepository, MessageRepository>();
services.AddScoped<IMessageReactionRepository, MessageReactionRepository>();
services.AddScoped<ModuleUserStatsService, UserStatsService>();
services.AddScoped<ContractUserStatsService>(sp => (ContractUserStatsService)sp.GetRequiredService<ModuleUserStatsService>());
services.AddScoped<ContractUserStatsService, UserStatsServiceAdapter>();
services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));

View File

@@ -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<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(List<Guid> 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)
);
}
}

View File

@@ -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;
}
}