Перепиливание под чистый DDD
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
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;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
|
||||
public record CleanRunCommand(IFileStorageService FileStorage, AuthDbContext IdentityDb) : ICommand<MessageResponse>;
|
||||
|
||||
internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand, MessageResponse>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
validUrls.Add(u!);
|
||||
}
|
||||
foreach (var u in activeChatUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
foreach (var u in activeUserUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
|
||||
var validFileIds = validUrls
|
||||
.Where(u => u.Contains("/api/files/"))
|
||||
.Select(u => u.Split('/').Last())
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var file in allMinioFiles)
|
||||
{
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
{
|
||||
await request.FileStorage.DeleteFileAsync(file.FileId);
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
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;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
|
||||
public record ResetUserPasswordCommand(Guid UserId, string NewPassword) : ICommand<SuccessResponse>;
|
||||
|
||||
internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUserPasswordCommand, SuccessResponse>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IAuthUnitOfWork _identityUnitOfWork;
|
||||
|
||||
public ResetUserPasswordCommandHandler(IUserRepository userRepository, IAuthUnitOfWork identityUnitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_identityUnitOfWork = identityUnitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<SuccessResponse>> Handle(ResetUserPasswordCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<SuccessResponse>(AuthErrors.UserNotFound);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
return Result.Failure<SuccessResponse>(DomainErrors.InvalidPassword);
|
||||
|
||||
var hash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
|
||||
user.ChangePassword(hash);
|
||||
|
||||
await _identityUnitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(new SuccessResponse(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
|
||||
namespace Host.Application.Admin.Commands;
|
||||
|
||||
public record UpdateSettingsCommand(SystemSettingsDto Settings) : ICommand<SystemSettingsDto>;
|
||||
|
||||
internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSettingsCommand, SystemSettingsDto>
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public UpdateSettingsCommandHandler(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task<Result<SystemSettingsDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await _settingsService.UpdateSettingsAsync(request.Settings, cancellationToken);
|
||||
return Result.Success(request.Settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public record AdminUserDetailsDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
DateTime CreatedAt,
|
||||
bool IsOnline,
|
||||
DateTime LastOnlineAt,
|
||||
AdminUserStatsDto Stats
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public record AdminUserDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Email,
|
||||
string? Avatar,
|
||||
DateTime CreatedAt,
|
||||
bool IsOnline,
|
||||
DateTime LastOnlineAt
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public record AdminUserStatsDto(
|
||||
int MessagesCount,
|
||||
int MediaCount,
|
||||
int FilesCount,
|
||||
int LinksCount,
|
||||
long StorageUsedBytes
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public class CleanupDryRunResultDto
|
||||
{
|
||||
public int OrphanedMessagesCount { get; set; }
|
||||
public long OrphanedMediaBytes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
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.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record CleanDryRunQuery(IFileStorageService FileStorage, AuthDbContext IdentityDb) : IQuery<CleanupDryRunResultDto>;
|
||||
|
||||
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanupDryRunResultDto>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
validUrls.Add(u!);
|
||||
}
|
||||
foreach (var u in activeChatUrls)
|
||||
{
|
||||
validUrls.Add(u);
|
||||
}
|
||||
foreach (var u in activeUserUrls)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!validFileIds.Contains(file.FileId))
|
||||
{
|
||||
safeBytes += file.Size;
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success(new CleanupDryRunResultDto
|
||||
{
|
||||
OrphanedMessagesCount = orphanedMessagesCount,
|
||||
OrphanedMediaBytes = safeBytes
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Services;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record GetDashboardStatsQuery() : IQuery<DashboardStatsDto>;
|
||||
|
||||
internal sealed class GetDashboardStatsQueryHandler : IQueryHandler<GetDashboardStatsQuery, DashboardStatsDto>
|
||||
{
|
||||
private readonly IStatisticsService _statisticsService;
|
||||
|
||||
public GetDashboardStatsQueryHandler(IStatisticsService statisticsService)
|
||||
{
|
||||
_statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
public async Task<Result<DashboardStatsDto>> Handle(GetDashboardStatsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var stats = await _statisticsService.GetDashboardStatsAsync(cancellationToken);
|
||||
return Result.Success(stats);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
|
||||
namespace Host.Application.Admin.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
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;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record GetUserDetailsQuery(Guid UserId) : IQuery<AdminUserDetailsDto>;
|
||||
|
||||
internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQuery, AdminUserDetailsDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public GetUserDetailsQueryHandler(IUserRepository userRepository, IMongoDatabase mongoDatabase)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_messages = mongoDatabase.GetCollection<Message>("Messages");
|
||||
}
|
||||
|
||||
public async Task<Result<AdminUserDetailsDto>> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (targetUser == null)
|
||||
{
|
||||
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);
|
||||
|
||||
var messagesCount = userMessages.Count;
|
||||
|
||||
var allUserMedia = userMessages.OfType<MediaMessage>().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))
|
||||
.ToList();
|
||||
|
||||
var linksCount = userContents.Count(c => !string.IsNullOrEmpty(c) && c.Contains("http"));
|
||||
|
||||
var result = new AdminUserDetailsDto(
|
||||
targetUser.Id,
|
||||
targetUser.Username,
|
||||
targetUser.DisplayName,
|
||||
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,
|
||||
new AdminUserStatsDto(
|
||||
messagesCount,
|
||||
mediaCount,
|
||||
filesCount,
|
||||
linksCount,
|
||||
storageUsed
|
||||
)
|
||||
);
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
|
||||
namespace Host.Application.Admin.Queries;
|
||||
|
||||
public record SearchUsersQuery(string Query) : IQuery<List<AdminUserDto>>;
|
||||
|
||||
internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery, List<AdminUserDto>>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public SearchUsersQueryHandler(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<AdminUserDto>>> Handle(SearchUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var users = string.IsNullOrWhiteSpace(request.Query)
|
||||
? await _userRepository.SearchUsersAsync("", cancellationToken)
|
||||
: await _userRepository.SearchUsersAsync(request.Query, cancellationToken);
|
||||
|
||||
var result = users.Select(u => new AdminUserDto(
|
||||
u.Id,
|
||||
u.Username,
|
||||
u.DisplayName,
|
||||
u.Email,
|
||||
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
|
||||
)).ToList();
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
7
backend/src/Modules/Admin/DependencyInjection.cs
Normal file
7
backend/src/Modules/Admin/DependencyInjection.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Modules.Admin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
public static class DependencyInjection {
|
||||
public static IServiceCollection AddAdminModule(this IServiceCollection services) {
|
||||
return services;
|
||||
}
|
||||
}
|
||||
22
backend/src/Modules/Admin/Knot.Modules.Admin.csproj
Normal file
22
backend/src/Modules/Admin/Knot.Modules.Admin.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>Knot.Modules.Admin.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,86 @@
|
||||
using Carter;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
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 Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
|
||||
public sealed class AdminEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("api/admin"); // Middleware handles auth
|
||||
|
||||
group.MapGet("settings", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetSettingsQuery(), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPut("settings", async ([FromBody] SystemSettingsDto settings, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new UpdateSettingsCommand(settings), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapGet("dashboard", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetDashboardStatsQuery(), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("users", async ([FromBody] RegisterUserCommand command, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(command, ct);
|
||||
if (result.IsFailure) return Results.BadRequest(new { error = result.Error.Description });
|
||||
|
||||
var userDetails = await sender.Send(new GetUserDetailsQuery(result.Value.User.Id), ct);
|
||||
return Results.Ok(userDetails.Value);
|
||||
});
|
||||
|
||||
group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordDto dto, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
if (result.Error.Code == "User.NotFound") return Results.NotFound(new { error = "User not found" });
|
||||
return Results.BadRequest(new { error = result.Error.Description });
|
||||
}
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) =>
|
||||
{
|
||||
var result = await sender.Send(new SearchUsersQuery(query), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapGet("users/{id:guid}", async (Guid id, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetUserDetailsQuery(id), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found");
|
||||
});
|
||||
|
||||
group.MapGet("clean/dry-run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new CleanDryRunQuery(fileStorage, identityDb), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("clean/run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new CleanRunCommand(fileStorage, identityDb), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user