Админка
This commit is contained in:
@@ -108,6 +108,12 @@ builder.Services.AddRouting(options =>
|
|||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||||
|
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddSignalR()
|
builder.Services.AddSignalR()
|
||||||
.AddJsonProtocol(options =>
|
.AddJsonProtocol(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,28 +8,36 @@ using System.Threading.Tasks;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Shared.Kernel.Storage;
|
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 MongoDB.Driver;
|
||||||
using Microsoft.EntityFrameworkCore;
|
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;
|
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>
|
internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand, MessageResponse>
|
||||||
{
|
{
|
||||||
private readonly ChatsDbContext _chatsDbContext;
|
private readonly ChatsDbContext _chatsDbContext;
|
||||||
private readonly IMongoCollection<Message> _messages;
|
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;
|
_chatsDbContext = chatsDbContext;
|
||||||
_messages = mongoDb.GetCollection<Message>("messages");
|
_messages = mongoDb.GetCollection<Message>("messages");
|
||||||
|
_stories = mongoDb.GetCollection<Story>("stories");
|
||||||
|
_fileStorage = fileStorage;
|
||||||
|
_identityDb = identityDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<MessageResponse>> Handle(CleanRunCommand request, CancellationToken cancellationToken)
|
public async Task<Result<MessageResponse>> Handle(CleanRunCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||||
@@ -37,16 +45,18 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
|||||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var orphanMessages = allMessages
|
var orphanMessages = allMessages
|
||||||
.Where(m => !activeChatIds.Contains(m.ChatId))
|
.Where(m => !activeChatIds.Contains(m.ChatId) || m.IsDeleted)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var keptMessages = allMessages
|
var keptMessages = allMessages
|
||||||
.Where(m => activeChatIds.Contains(m.ChatId))
|
.Where(m => activeChatIds.Contains(m.ChatId) && !m.IsDeleted)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList();
|
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
||||||
|
|
||||||
var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
var allUsers = await _identityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var allStories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var validUrls = new HashSet<string>();
|
var validUrls = new HashSet<string>();
|
||||||
|
|
||||||
@@ -64,18 +74,14 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
|||||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||||
.Select(u => u.Avatar!);
|
.Select(u => u.Avatar!);
|
||||||
|
|
||||||
foreach (var u in activeMessageUrls)
|
var activeStoryUrls = allStories
|
||||||
{
|
.Where(s => !string.IsNullOrEmpty(s.MediaUrl))
|
||||||
validUrls.Add(u!);
|
.Select(s => s.MediaUrl!);
|
||||||
}
|
|
||||||
foreach (var u in activeChatUrls)
|
foreach (var u in activeMessageUrls) validUrls.Add(u!);
|
||||||
{
|
foreach (var u in activeChatUrls) validUrls.Add(u);
|
||||||
validUrls.Add(u);
|
foreach (var u in activeUserUrls) validUrls.Add(u);
|
||||||
}
|
foreach (var u in activeStoryUrls) validUrls.Add(u);
|
||||||
foreach (var u in activeUserUrls)
|
|
||||||
{
|
|
||||||
validUrls.Add(u);
|
|
||||||
}
|
|
||||||
|
|
||||||
var validFileIds = validUrls
|
var validFileIds = validUrls
|
||||||
.Where(u => u.Contains("/api/files/"))
|
.Where(u => u.Contains("/api/files/"))
|
||||||
@@ -86,7 +92,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
|||||||
{
|
{
|
||||||
if (!validFileIds.Contains(file.FileId))
|
if (!validFileIds.Contains(file.FileId))
|
||||||
{
|
{
|
||||||
await request.FileStorage.DeleteFileAsync(file.FileId);
|
await _fileStorage.DeleteFileAsync(file.FileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,4 +105,9 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
|||||||
|
|
||||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Failure<MessageResponse>(new Error("Cleanup.ProcessError", $"Cleanup failed: {ex.Message}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -8,7 +8,6 @@ using Knot.Shared.Kernel;
|
|||||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||||
|
|
||||||
using Knot.Modules.Auth.Application.Users;
|
using Knot.Modules.Auth.Application.Users;
|
||||||
using Knot.Modules.Auth.Domain;
|
|
||||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||||
using Knot.Modules.Auth.Application.Abstractions;
|
using Knot.Modules.Auth.Application.Abstractions;
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -8,7 +8,7 @@ using Knot.Modules.Stories.Application.Abstractions;
|
|||||||
|
|
||||||
namespace Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
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>
|
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)
|
public async Task<Result<bool>> Handle(TestKlipyConnectionCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var settings = await _settingsService.GetSettingsAsync(ct);
|
if (string.IsNullOrEmpty(request.ApiKey))
|
||||||
if (string.IsNullOrEmpty(settings.Klipy.ApiKey))
|
|
||||||
{
|
{
|
||||||
return Result.Failure<bool>(new Error("Klipy.KeyMissing", "API Key for Klipy is not configured."));
|
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)
|
if (!isOk)
|
||||||
{
|
{
|
||||||
return Result.Failure<bool>(new Error("Klipy.AuthFailed", "Klipy API test request failed. Verify your API key."));
|
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,
|
DateTime CreatedAt,
|
||||||
bool IsOnline,
|
bool IsOnline,
|
||||||
DateTime LastOnlineAt,
|
DateTime LastOnlineAt,
|
||||||
|
bool IsBanned,
|
||||||
AdminUserStatsDto Stats
|
AdminUserStatsDto Stats
|
||||||
);
|
);
|
||||||
@@ -10,5 +10,6 @@ public record AdminUserDto(
|
|||||||
string? Avatar,
|
string? Avatar,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
bool IsOnline,
|
bool IsOnline,
|
||||||
DateTime LastOnlineAt
|
DateTime LastOnlineAt,
|
||||||
|
bool IsBanned
|
||||||
);
|
);
|
||||||
+32
-22
@@ -8,31 +8,38 @@ using System.Threading.Tasks;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Shared.Kernel.Storage;
|
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 MongoDB.Driver;
|
||||||
using Microsoft.EntityFrameworkCore;
|
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.Auth.DTOs;
|
||||||
|
|
||||||
using Knot.Modules.Auth.Application.Users;
|
using Knot.Modules.Auth.Application.Users;
|
||||||
|
|
||||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
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>
|
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanupDryRunResultDto>
|
||||||
{
|
{
|
||||||
private readonly ChatsDbContext _chatsDbContext;
|
private readonly ChatsDbContext _chatsDbContext;
|
||||||
private readonly IMongoCollection<Message> _messages;
|
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;
|
_chatsDbContext = chatsDbContext;
|
||||||
_messages = mongoDb.GetCollection<Message>("messages");
|
_messages = mongoDb.GetCollection<Message>("messages");
|
||||||
|
_stories = mongoDb.GetCollection<Story>("stories");
|
||||||
|
_fileStorage = fileStorage;
|
||||||
|
_identityDb = identityDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken cancellationToken)
|
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||||
@@ -40,18 +47,20 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
|||||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var orphanedMessages = allMessages
|
var orphanedMessages = allMessages
|
||||||
.Where(m => !activeChatIds.Contains(m.ChatId))
|
.Where(m => !activeChatIds.Contains(m.ChatId) || m.IsDeleted)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var keptMessages = allMessages
|
var keptMessages = allMessages
|
||||||
.Where(m => activeChatIds.Contains(m.ChatId))
|
.Where(m => activeChatIds.Contains(m.ChatId) && !m.IsDeleted)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var orphanedMessagesCount = orphanedMessages.Count;
|
var orphanedMessagesCount = orphanedMessages.Count;
|
||||||
|
|
||||||
var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList();
|
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
||||||
|
|
||||||
var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
var allUsers = await _identityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var allStories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var validUrls = new HashSet<string>();
|
var validUrls = new HashSet<string>();
|
||||||
|
|
||||||
@@ -69,18 +78,14 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
|||||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||||
.Select(u => u.Avatar!);
|
.Select(u => u.Avatar!);
|
||||||
|
|
||||||
foreach (var u in activeMessageUrls)
|
var activeStoryUrls = allStories
|
||||||
{
|
.Where(s => !string.IsNullOrEmpty(s.MediaUrl))
|
||||||
validUrls.Add(u!);
|
.Select(s => s.MediaUrl!);
|
||||||
}
|
|
||||||
foreach (var u in activeChatUrls)
|
foreach (var u in activeMessageUrls) validUrls.Add(u!);
|
||||||
{
|
foreach (var u in activeChatUrls) validUrls.Add(u);
|
||||||
validUrls.Add(u);
|
foreach (var u in activeUserUrls) validUrls.Add(u);
|
||||||
}
|
foreach (var u in activeStoryUrls) validUrls.Add(u);
|
||||||
foreach (var u in activeUserUrls)
|
|
||||||
{
|
|
||||||
validUrls.Add(u);
|
|
||||||
}
|
|
||||||
|
|
||||||
var validFileIds = validUrls
|
var validFileIds = validUrls
|
||||||
.Where(u => u.Contains("/api/files/"))
|
.Where(u => u.Contains("/api/files/"))
|
||||||
@@ -102,4 +107,9 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
|||||||
OrphanedMediaBytes = safeBytes
|
OrphanedMediaBytes = safeBytes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Failure<CleanupDryRunResultDto>(new Error("Cleanup.ProcessError", $"Failed to analyze junk data: {ex.Message}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-10
@@ -10,7 +10,6 @@ using Knot.Shared.Kernel;
|
|||||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||||
|
|
||||||
using Knot.Modules.Auth.Application.Users;
|
using Knot.Modules.Auth.Application.Users;
|
||||||
using Knot.Modules.Auth.Domain;
|
|
||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Modules.Conversations.Domain;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
|
||||||
@@ -37,32 +36,49 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
|
|||||||
return Result.Failure<AdminUserDetailsDto>(AuthErrors.UserNotFound);
|
return Result.Failure<AdminUserDetailsDto>(AuthErrors.UserNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Message> userMessages;
|
||||||
|
try
|
||||||
|
{
|
||||||
var filter = Builders<Message>.Filter.Eq(m => m.SenderId, request.UserId);
|
var filter = Builders<Message>.Filter.Eq(m => m.SenderId, request.UserId);
|
||||||
var userMessages = await _messages.Find(filter).ToListAsync(cancellationToken);
|
userMessages = await _messages.Find(filter).ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
userMessages = new List<Message>();
|
||||||
|
}
|
||||||
|
|
||||||
var messagesCount = userMessages.Count;
|
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 mediaCount = allUserMedia.Count(m => m.Type == "image" || m.Type == "video");
|
||||||
var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio");
|
var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio");
|
||||||
var storageUsed = allUserMedia.Sum(m => m.Size ?? 0);
|
var storageUsed = allUserMedia.Sum(m => m.Size ?? 0);
|
||||||
|
|
||||||
var userContents = userMessages.OfType<TextMessage>().Select(m => m.Content)
|
var userContents = userMessages.OfType<TextMessage>()
|
||||||
.Concat(userMessages.OfType<MediaMessage>().Where(m => m.Caption != null).Select(m => m.Caption))
|
.Select(m => m.Content)
|
||||||
|
.Concat(mediaMessages.Where(m => m.Caption != null).Select(m => m.Caption))
|
||||||
|
.Where(c => c != null)
|
||||||
.ToList();
|
.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(
|
var result = new AdminUserDetailsDto(
|
||||||
targetUser.Id,
|
targetUser.Id,
|
||||||
targetUser.Username,
|
targetUser.Username ?? "unknown",
|
||||||
targetUser.DisplayName,
|
targetUser.DisplayName ?? "User",
|
||||||
targetUser.Bio,
|
targetUser.Bio,
|
||||||
targetUser.Avatar,
|
targetUser.Avatar,
|
||||||
targetUser.CreatedAt,
|
targetUser.CreatedAt,
|
||||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()),
|
isOnline,
|
||||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString()) ? DateTime.UtcNow : targetUser.CreatedAt,
|
isOnline ? DateTime.UtcNow : (targetUser.LastSeen ?? targetUser.CreatedAt),
|
||||||
|
targetUser.IsBanned,
|
||||||
new AdminUserStatsDto(
|
new AdminUserStatsDto(
|
||||||
messagesCount,
|
messagesCount,
|
||||||
mediaCount,
|
mediaCount,
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery,
|
|||||||
u.Avatar,
|
u.Avatar,
|
||||||
u.CreatedAt,
|
u.CreatedAt,
|
||||||
Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(u.Id.ToString()),
|
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();
|
)).ToList();
|
||||||
|
|
||||||
return Result.Success(result);
|
return Result.Success(result);
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ using Microsoft.AspNetCore.Routing;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||||
|
|
||||||
|
using Knot.Modules.Conversations.Application.Users.Commands.DeleteUser;
|
||||||
|
|
||||||
namespace Knot.Host.Presentation.Endpoints;
|
namespace Knot.Host.Presentation.Endpoints;
|
||||||
|
|
||||||
|
public record KlipyTestDto(string ApiKey, string AppName);
|
||||||
|
|
||||||
public sealed class AdminEndpoints : ICarterModule
|
public sealed class AdminEndpoints : ICarterModule
|
||||||
{
|
{
|
||||||
public void AddRoutes(IEndpointRouteBuilder app)
|
public void AddRoutes(IEndpointRouteBuilder app)
|
||||||
@@ -35,9 +39,9 @@ public sealed class AdminEndpoints : ICarterModule
|
|||||||
return Results.Ok(result.Value);
|
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 });
|
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);
|
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) =>
|
group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new SearchUsersQuery(query), ct);
|
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");
|
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);
|
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);
|
return Results.Ok(result.Value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ public sealed class User : AggregateRoot<Guid>
|
|||||||
public bool IsOnline { get; private set; }
|
public bool IsOnline { get; private set; }
|
||||||
public DateTime? LastSeen { get; private set; }
|
public DateTime? LastSeen { get; private set; }
|
||||||
public bool HideStatus { get; private set; }
|
public bool HideStatus { get; private set; }
|
||||||
|
public bool IsBanned { get; private set; }
|
||||||
|
|
||||||
|
public void Ban() => IsBanned = true;
|
||||||
|
public void Unban() => IsBanned = false;
|
||||||
|
|
||||||
private User(Guid id, string username, string passwordHash, string displayName, string? email, string? bio = null)
|
private User(Guid id, string username, string passwordHash, string displayName, string? email, string? bio = null)
|
||||||
: base(id)
|
: base(id)
|
||||||
@@ -34,6 +38,7 @@ public sealed class User : AggregateRoot<Guid>
|
|||||||
Email = email;
|
Email = email;
|
||||||
Bio = bio;
|
Bio = bio;
|
||||||
CreatedAt = DateTime.UtcNow;
|
CreatedAt = DateTime.UtcNow;
|
||||||
|
IsBanned = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AuthDbContext))]
|
||||||
|
[Migration("20270327160000_AddUserBanField")]
|
||||||
|
partial class AddUserBanField
|
||||||
|
{
|
||||||
|
/// <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<string>("Bio")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Birthday")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
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>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", "identity");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Knot.Modules.Auth.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserBanField : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsBanned",
|
||||||
|
schema: "identity",
|
||||||
|
table: "Users",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsBanned",
|
||||||
|
schema: "identity",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,9 @@ namespace Knot.Modules.Auth.Migrations
|
|||||||
b.Property<bool>("IsExternal")
|
b.Property<bool>("IsExternal")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBanned")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("IsOnline")
|
b.Property<bool>("IsOnline")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class MessagesConfig
|
|||||||
public int ChatMessageLimit { get; set; } = 0;
|
public int ChatMessageLimit { get; set; } = 0;
|
||||||
public bool AllowMedia { get; set; } = true;
|
public bool AllowMedia { get; set; } = true;
|
||||||
public int MaxMediaSizeBytes { get; set; } = 50 * 1024 * 1024;
|
public int MaxMediaSizeBytes { get; set; } = 50 * 1024 * 1024;
|
||||||
|
public int MaxFileSize { get; set; } = 100 * 1024 * 1024;
|
||||||
public List<string> AllowedMediaTypes { get; set; } = new() { "image/jpeg", "image/png", "video/mp4", "image/gif" };
|
public List<string> AllowedMediaTypes { get; set; } = new() { "image/jpeg", "image/png", "video/mp4", "image/gif" };
|
||||||
public bool AllowVoiceMessages { get; set; } = true;
|
public bool AllowVoiceMessages { get; set; } = true;
|
||||||
public bool AllowForwarding { get; set; } = true;
|
public bool AllowForwarding { get; set; } = true;
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ namespace Knot.Modules.Stories.Application.Abstractions;
|
|||||||
|
|
||||||
public interface IKlipyClient
|
public interface IKlipyClient
|
||||||
{
|
{
|
||||||
Task<bool> TestConnectionAsync(string apiKey, CancellationToken ct = default);
|
Task<bool> TestConnectionAsync(string apiKey, string appName, CancellationToken ct = default);
|
||||||
Task<List<string>> SearchVideosAsync(string apiKey, string query, int limit, CancellationToken ct = default);
|
Task<List<string>> SearchVideosAsync(string apiKey, string appName, string query, int limit, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Knot.Modules.Stories.Application.Abstractions;
|
using Knot.Modules.Stories.Application.Abstractions;
|
||||||
|
using Knot.Modules.Stories.Infrastructure.External;
|
||||||
using Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
|
using Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
namespace Knot.Modules.Stories;
|
namespace Knot.Modules.Stories;
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ public static class DependencyInjection
|
|||||||
public static IServiceCollection AddStoriesModule(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddStoriesModule(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddScoped<IStoryRepository, StoryRepository>();
|
services.AddScoped<IStoryRepository, StoryRepository>();
|
||||||
|
services.AddHttpClient<IKlipyClient, KlipyClient>();
|
||||||
|
|
||||||
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||||
services.AddDbContext<Infrastructure.Database.StoriesDbContext>(options =>
|
services.AddDbContext<Infrastructure.Database.StoriesDbContext>(options =>
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ public sealed class KlipyClient : IKlipyClient
|
|||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> TestConnectionAsync(string apiKey, CancellationToken ct = default)
|
public async Task<bool> TestConnectionAsync(string apiKey, string appName, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.klipy.co/v1/trending?limit=1");
|
// According to docs: https://api.klipy.com/api/v1/{app_key}/gifs/trending
|
||||||
request.Headers.Add("X-API-KEY", apiKey);
|
var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.klipy.com/api/v1/{appName}/gifs/trending?page=1&per_page=1&customer_id=knot_admin_test");
|
||||||
|
request.Headers.Add("X-KLIPY-API-KEY", apiKey);
|
||||||
|
|
||||||
using var response = await _httpClient.SendAsync(request, ct);
|
using var response = await _httpClient.SendAsync(request, ct);
|
||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
@@ -32,9 +33,9 @@ public sealed class KlipyClient : IKlipyClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<string>> SearchVideosAsync(string apiKey, string query, int limit, CancellationToken ct = default)
|
public async Task<List<string>> SearchVideosAsync(string apiKey, string appName, string query, int limit, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// В реальном проекте: десериализация ответа от Klipy API
|
// To be implemented as needed
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export class HttpClient {
|
|||||||
|
|
||||||
const isFormData = fetchOptions.body instanceof FormData;
|
const isFormData = fetchOptions.body instanceof FormData;
|
||||||
const computedHeaders: Record<string, string> = {
|
const computedHeaders: Record<string, string> = {
|
||||||
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
...(this.token
|
||||||
|
? { Authorization: this.token.startsWith('Basic ') ? this.token : `Bearer ${this.token}` }
|
||||||
|
: {}),
|
||||||
...(fetchOptions.headers as Record<string, string>),
|
...(fetchOptions.headers as Record<string, string>),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user