Админка
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record BanUserCommand(Guid UserId) : ICommand<Result>;
|
||||
|
||||
internal sealed class BanUserCommandHandler : ICommandHandler<BanUserCommand, Result>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IAuthUnitOfWork _unitOfWork;
|
||||
|
||||
public BanUserCommandHandler(IUserRepository userRepository, IAuthUnitOfWork unitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<Result>> Handle(BanUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null) return Result.Failure<Result>(Error.NotFound("User.NotFound", "User not found"));
|
||||
|
||||
user.Ban();
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(Result.Success());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record UnbanUserCommand(Guid UserId) : ICommand<Result>;
|
||||
|
||||
internal sealed class UnbanUserCommandHandler : ICommandHandler<UnbanUserCommand, Result>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IAuthUnitOfWork _unitOfWork;
|
||||
|
||||
public UnbanUserCommandHandler(IUserRepository userRepository, IAuthUnitOfWork unitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<Result>> Handle(UnbanUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null) return Result.Failure<Result>(Error.NotFound("User.NotFound", "User not found"));
|
||||
|
||||
user.Unban();
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(Result.Success());
|
||||
}
|
||||
}
|
||||
@@ -8,95 +8,106 @@ using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Stories.Domain;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record CleanRunCommand(IFileStorageService FileStorage, AuthDbContext IdentityDb) : ICommand<MessageResponse>;
|
||||
public record CleanRunCommand() : ICommand<MessageResponse>;
|
||||
|
||||
internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand, MessageResponse>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
private readonly IMongoCollection<Story> _stories;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly AuthDbContext _identityDb;
|
||||
|
||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb, IFileStorageService fileStorage, AuthDbContext identityDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
_stories = mongoDb.GetCollection<Story>("stories");
|
||||
_fileStorage = fileStorage;
|
||||
_identityDb = identityDb;
|
||||
}
|
||||
|
||||
public async Task<Result<MessageResponse>> Handle(CleanRunCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var orphanMessages = allMessages
|
||||
.Where(m => !activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var keptMessages = allMessages
|
||||
.Where(m => activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
.Where(u => !string.IsNullOrEmpty(u));
|
||||
|
||||
var activeChatUrls = allChats
|
||||
.Where(c => !string.IsNullOrEmpty(c.Avatar))
|
||||
.Select(c => c.Avatar!);
|
||||
|
||||
var activeUserUrls = allUsers
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar!);
|
||||
|
||||
foreach (var u in activeMessageUrls)
|
||||
try
|
||||
{
|
||||
validUrls.Add(u!);
|
||||
}
|
||||
foreach (var u in activeChatUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
foreach (var u in activeUserUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var validFileIds = validUrls
|
||||
.Where(u => u.Contains("/api/files/"))
|
||||
.Select(u => u.Split('/').Last())
|
||||
.ToHashSet();
|
||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var file in allMinioFiles)
|
||||
{
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
var orphanMessages = allMessages
|
||||
.Where(m => !activeChatIds.Contains(m.ChatId) || m.IsDeleted)
|
||||
.ToList();
|
||||
|
||||
var keptMessages = allMessages
|
||||
.Where(m => activeChatIds.Contains(m.ChatId) && !m.IsDeleted)
|
||||
.ToList();
|
||||
|
||||
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await _identityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
|
||||
var allStories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
.Where(u => !string.IsNullOrEmpty(u));
|
||||
|
||||
var activeChatUrls = allChats
|
||||
.Where(c => !string.IsNullOrEmpty(c.Avatar))
|
||||
.Select(c => c.Avatar!);
|
||||
|
||||
var activeUserUrls = allUsers
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar!);
|
||||
|
||||
var activeStoryUrls = allStories
|
||||
.Where(s => !string.IsNullOrEmpty(s.MediaUrl))
|
||||
.Select(s => s.MediaUrl!);
|
||||
|
||||
foreach (var u in activeMessageUrls) validUrls.Add(u!);
|
||||
foreach (var u in activeChatUrls) validUrls.Add(u);
|
||||
foreach (var u in activeUserUrls) validUrls.Add(u);
|
||||
foreach (var u in activeStoryUrls) validUrls.Add(u);
|
||||
|
||||
var validFileIds = validUrls
|
||||
.Where(u => u.Contains("/api/files/"))
|
||||
.Select(u => u.Split('/').Last())
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var file in allMinioFiles)
|
||||
{
|
||||
await request.FileStorage.DeleteFileAsync(file.FileId);
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
{
|
||||
await _fileStorage.DeleteFileAsync(file.FileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (orphanMessages.Any())
|
||||
if (orphanMessages.Any())
|
||||
{
|
||||
var orphanIds = orphanMessages.Select(m => m.Id).ToList();
|
||||
var filter = Builders<Message>.Filter.In(m => m.Id, orphanIds);
|
||||
await _messages.DeleteManyAsync(filter, cancellationToken);
|
||||
}
|
||||
|
||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var orphanIds = orphanMessages.Select(m => m.Id).ToList();
|
||||
var filter = Builders<Message>.Filter.In(m => m.Id, orphanIds);
|
||||
await _messages.DeleteManyAsync(filter, cancellationToken);
|
||||
return Result.Failure<MessageResponse>(new Error("Cleanup.ProcessError", $"Cleanup failed: {ex.Message}"));
|
||||
}
|
||||
|
||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using Knot.Modules.Stories.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||
|
||||
public record TestKlipyConnectionCommand() : ICommand<bool>;
|
||||
public record TestKlipyConnectionCommand(string ApiKey, string AppName) : ICommand<bool>;
|
||||
|
||||
internal sealed class TestKlipyConnectionCommandHandler : ICommandHandler<TestKlipyConnectionCommand, bool>
|
||||
{
|
||||
@@ -23,13 +23,12 @@ internal sealed class TestKlipyConnectionCommandHandler : ICommandHandler<TestKl
|
||||
|
||||
public async Task<Result<bool>> Handle(TestKlipyConnectionCommand request, CancellationToken ct)
|
||||
{
|
||||
var settings = await _settingsService.GetSettingsAsync(ct);
|
||||
if (string.IsNullOrEmpty(settings.Klipy.ApiKey))
|
||||
if (string.IsNullOrEmpty(request.ApiKey))
|
||||
{
|
||||
return Result.Failure<bool>(new Error("Klipy.KeyMissing", "API Key for Klipy is not configured."));
|
||||
}
|
||||
|
||||
var isOk = await _klipyClient.TestConnectionAsync(settings.Klipy.ApiKey, ct);
|
||||
var isOk = await _klipyClient.TestConnectionAsync(request.ApiKey, request.AppName, ct);
|
||||
if (!isOk)
|
||||
{
|
||||
return Result.Failure<bool>(new Error("Klipy.AuthFailed", "Klipy API test request failed. Verify your API key."));
|
||||
|
||||
@@ -11,5 +11,6 @@ public record AdminUserDetailsDto(
|
||||
DateTime CreatedAt,
|
||||
bool IsOnline,
|
||||
DateTime LastOnlineAt,
|
||||
bool IsBanned,
|
||||
AdminUserStatsDto Stats
|
||||
);
|
||||
@@ -10,5 +10,6 @@ public record AdminUserDto(
|
||||
string? Avatar,
|
||||
DateTime CreatedAt,
|
||||
bool IsOnline,
|
||||
DateTime LastOnlineAt
|
||||
DateTime LastOnlineAt,
|
||||
bool IsBanned
|
||||
);
|
||||
@@ -8,98 +8,108 @@ using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Stories.Domain;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
public record CleanDryRunQuery(IFileStorageService FileStorage, AuthDbContext IdentityDb) : IQuery<CleanupDryRunResultDto>;
|
||||
public record CleanDryRunQuery() : IQuery<CleanupDryRunResultDto>;
|
||||
|
||||
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanupDryRunResultDto>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
private readonly IMongoCollection<Story> _stories;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly AuthDbContext _identityDb;
|
||||
|
||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb, IFileStorageService fileStorage, AuthDbContext identityDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
_stories = mongoDb.GetCollection<Story>("stories");
|
||||
_fileStorage = fileStorage;
|
||||
_identityDb = identityDb;
|
||||
}
|
||||
|
||||
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var orphanedMessages = allMessages
|
||||
.Where(m => !activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var keptMessages = allMessages
|
||||
.Where(m => activeChatIds.Contains(m.ChatId))
|
||||
.ToList();
|
||||
|
||||
var orphanedMessagesCount = orphanedMessages.Count;
|
||||
|
||||
var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
.Where(u => !string.IsNullOrEmpty(u));
|
||||
|
||||
var activeChatUrls = allChats
|
||||
.Where(c => !string.IsNullOrEmpty(c.Avatar))
|
||||
.Select(c => c.Avatar!);
|
||||
|
||||
var activeUserUrls = allUsers
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar!);
|
||||
|
||||
foreach (var u in activeMessageUrls)
|
||||
try
|
||||
{
|
||||
validUrls.Add(u!);
|
||||
}
|
||||
foreach (var u in activeChatUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
foreach (var u in activeUserUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var validFileIds = validUrls
|
||||
.Where(u => u.Contains("/api/files/"))
|
||||
.Select(u => u.Split('/').Last())
|
||||
.ToHashSet();
|
||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
long safeBytes = 0;
|
||||
foreach (var file in allMinioFiles)
|
||||
{
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
var orphanedMessages = allMessages
|
||||
.Where(m => !activeChatIds.Contains(m.ChatId) || m.IsDeleted)
|
||||
.ToList();
|
||||
|
||||
var keptMessages = allMessages
|
||||
.Where(m => activeChatIds.Contains(m.ChatId) && !m.IsDeleted)
|
||||
.ToList();
|
||||
|
||||
var orphanedMessagesCount = orphanedMessages.Count;
|
||||
|
||||
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await _identityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
|
||||
var allStories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
.Where(u => !string.IsNullOrEmpty(u));
|
||||
|
||||
var activeChatUrls = allChats
|
||||
.Where(c => !string.IsNullOrEmpty(c.Avatar))
|
||||
.Select(c => c.Avatar!);
|
||||
|
||||
var activeUserUrls = allUsers
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar!);
|
||||
|
||||
var activeStoryUrls = allStories
|
||||
.Where(s => !string.IsNullOrEmpty(s.MediaUrl))
|
||||
.Select(s => s.MediaUrl!);
|
||||
|
||||
foreach (var u in activeMessageUrls) validUrls.Add(u!);
|
||||
foreach (var u in activeChatUrls) validUrls.Add(u);
|
||||
foreach (var u in activeUserUrls) validUrls.Add(u);
|
||||
foreach (var u in activeStoryUrls) validUrls.Add(u);
|
||||
|
||||
var validFileIds = validUrls
|
||||
.Where(u => u.Contains("/api/files/"))
|
||||
.Select(u => u.Split('/').Last())
|
||||
.ToHashSet();
|
||||
|
||||
long safeBytes = 0;
|
||||
foreach (var file in allMinioFiles)
|
||||
{
|
||||
safeBytes += file.Size;
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
{
|
||||
safeBytes += file.Size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success(new CleanupDryRunResultDto
|
||||
return Result.Success(new CleanupDryRunResultDto
|
||||
{
|
||||
OrphanedMessagesCount = orphanedMessagesCount,
|
||||
OrphanedMediaBytes = safeBytes
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OrphanedMessagesCount = orphanedMessagesCount,
|
||||
OrphanedMediaBytes = safeBytes
|
||||
});
|
||||
return Result.Failure<CleanupDryRunResultDto>(new Error("Cleanup.ProcessError", $"Failed to analyze junk data: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
|
||||
@@ -37,32 +36,49 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
|
||||
return Result.Failure<AdminUserDetailsDto>(AuthErrors.UserNotFound);
|
||||
}
|
||||
|
||||
var filter = Builders<Message>.Filter.Eq(m => m.SenderId, request.UserId);
|
||||
var userMessages = await _messages.Find(filter).ToListAsync(cancellationToken);
|
||||
List<Message> userMessages;
|
||||
try
|
||||
{
|
||||
var filter = Builders<Message>.Filter.Eq(m => m.SenderId, request.UserId);
|
||||
userMessages = await _messages.Find(filter).ToListAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
userMessages = new List<Message>();
|
||||
}
|
||||
|
||||
var messagesCount = userMessages.Count;
|
||||
|
||||
var allUserMedia = userMessages.OfType<MediaMessage>().SelectMany(m => m.Media).ToList();
|
||||
var mediaMessages = userMessages.OfType<MediaMessage>().ToList();
|
||||
var allUserMedia = mediaMessages
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.ToList();
|
||||
|
||||
var mediaCount = allUserMedia.Count(m => m.Type == "image" || m.Type == "video");
|
||||
var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio");
|
||||
var storageUsed = allUserMedia.Sum(m => m.Size ?? 0);
|
||||
|
||||
var userContents = userMessages.OfType<TextMessage>().Select(m => m.Content)
|
||||
.Concat(userMessages.OfType<MediaMessage>().Where(m => m.Caption != null).Select(m => m.Caption))
|
||||
var userContents = userMessages.OfType<TextMessage>()
|
||||
.Select(m => m.Content)
|
||||
.Concat(mediaMessages.Where(m => m.Caption != null).Select(m => m.Caption))
|
||||
.Where(c => c != null)
|
||||
.ToList();
|
||||
|
||||
var linksCount = userContents.Count(c => !string.IsNullOrEmpty(c) && c.Contains("http"));
|
||||
var linksCount = userContents.Count(c => c != null && c.Contains("http", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var isOnline = Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString());
|
||||
|
||||
var result = new AdminUserDetailsDto(
|
||||
targetUser.Id,
|
||||
targetUser.Username,
|
||||
targetUser.DisplayName,
|
||||
targetUser.Username ?? "unknown",
|
||||
targetUser.DisplayName ?? "User",
|
||||
targetUser.Bio,
|
||||
targetUser.Avatar,
|
||||
targetUser.CreatedAt,
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()),
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()) ? DateTime.UtcNow : targetUser.CreatedAt,
|
||||
isOnline,
|
||||
isOnline ? DateTime.UtcNow : (targetUser.LastSeen ?? targetUser.CreatedAt),
|
||||
targetUser.IsBanned,
|
||||
new AdminUserStatsDto(
|
||||
messagesCount,
|
||||
mediaCount,
|
||||
|
||||
@@ -38,7 +38,8 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery,
|
||||
u.Avatar,
|
||||
u.CreatedAt,
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()),
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()) ? DateTime.UtcNow : u.CreatedAt
|
||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()) ? DateTime.UtcNow : u.CreatedAt,
|
||||
u.IsBanned
|
||||
)).ToList();
|
||||
|
||||
return Result.Success(result);
|
||||
|
||||
@@ -15,8 +15,12 @@ using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||
|
||||
using Knot.Modules.Conversations.Application.Users.Commands.DeleteUser;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
|
||||
public record KlipyTestDto(string ApiKey, string AppName);
|
||||
|
||||
public sealed class AdminEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
@@ -35,9 +39,9 @@ public sealed class AdminEndpoints : ICarterModule
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("settings/test-klipy", async (ISender sender, CancellationToken ct) =>
|
||||
group.MapPost("settings/test-klipy", async ([FromBody] KlipyTestDto dto, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new TestKlipyConnectionCommand(), ct);
|
||||
var result = await sender.Send(new TestKlipyConnectionCommand(dto.ApiKey, dto.AppName), ct);
|
||||
return result.IsSuccess ? Results.Ok(new { success = true }) : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
|
||||
@@ -67,6 +71,24 @@ public sealed class AdminEndpoints : ICarterModule
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("users/{userId:guid}/ban", async (Guid userId, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new BanUserCommand(userId), ct);
|
||||
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
|
||||
group.MapPost("users/{userId:guid}/unban", async (Guid userId, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new UnbanUserCommand(userId), ct);
|
||||
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
|
||||
group.MapDelete("users/{userId:guid}", async (Guid userId, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new DeleteUserCommand(userId), ct);
|
||||
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
|
||||
group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) =>
|
||||
{
|
||||
var result = await sender.Send(new SearchUsersQuery(query), ct);
|
||||
@@ -79,15 +101,25 @@ public sealed class AdminEndpoints : ICarterModule
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found");
|
||||
});
|
||||
|
||||
group.MapGet("clean/dry-run", async ([FromServices] IFileStorageService fileStorage, [FromServices] AuthDbContext identityDb, ISender sender, CancellationToken ct) =>
|
||||
group.MapGet("clean/dry-run", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new CleanDryRunQuery(fileStorage, identityDb), ct);
|
||||
var result = await sender.Send(new CleanDryRunQuery(), ct);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
Console.WriteLine($"[Admin] Cleanup Dry Run Error: {result.Error.Description}");
|
||||
return Results.BadRequest(new { error = result.Error.Description });
|
||||
}
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("clean/run", async ([FromServices] IFileStorageService fileStorage, [FromServices] AuthDbContext identityDb, ISender sender, CancellationToken ct) =>
|
||||
group.MapPost("clean/run", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new CleanRunCommand(fileStorage, identityDb), ct);
|
||||
var result = await sender.Send(new CleanRunCommand(), ct);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
Console.WriteLine($"[Admin] Cleanup Run Error: {result.Error.Description}");
|
||||
return Results.BadRequest(new { error = result.Error.Description });
|
||||
}
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user