mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 19:54:55 +00:00
Refactor friend request services and add SignalR error handling
Split IFriendRequestService into command and query interfaces, refactor related services and tests, and update dependency injection. Add HubResult response model and a SignalR HubExceptionFilter for consistent error handling. Move and update FriendsHub to use new command service and result pattern. Update username validation to allow digits after Cyrillic letters. Add new controllers and configuration for SignalR. Remove obsolete IFriendRequestService and related code.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Govor.API.Controllers.Friends;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/friends")]
|
||||
public class FriendsRequestQueryController : Controller
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Govor.API.Controllers.Friends;
|
||||
|
||||
public class FriendshipController
|
||||
{
|
||||
|
||||
}
|
||||
@@ -41,7 +41,8 @@ public static class ConfigurationProgramExtensions
|
||||
|
||||
// Friends services
|
||||
services.AddScoped<IFriendshipService, FriendshipService>();
|
||||
services.AddScoped<IFriendRequestService, FriendRequestService>();
|
||||
services.AddScoped<IFriendRequestCommandService, FriendRequestCommandService>();
|
||||
services.AddScoped<IFriendRequestQueryService, FriendRequestQueryService>();
|
||||
services.AddScoped<IFriendsBlockService, FriendsBlockService>();
|
||||
|
||||
services.AddScoped<IStorageService>(sp =>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using Govor.API.Filters;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Govor.API.Extensions;
|
||||
|
||||
public static class ConfigurationSignalR
|
||||
{
|
||||
public static void AddSignalRConf(this IServiceCollection services)
|
||||
{
|
||||
services.AddSignalR(options =>
|
||||
{
|
||||
options.AddFilter<HubExceptionFilter>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Govor.Contracts.Responses.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Govor.API.Filters;
|
||||
|
||||
public class HubExceptionFilter : IHubFilter
|
||||
{
|
||||
private readonly ILogger<HubExceptionFilter> _logger;
|
||||
|
||||
public HubExceptionFilter(ILogger<HubExceptionFilter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask<object?> InvokeMethodAsync(
|
||||
HubInvocationContext context,
|
||||
Func<HubInvocationContext, ValueTask<object?>> next)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await next(context);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Bad request in {Method}", context.HubMethodName);
|
||||
return CreateHubErrorResult(context, HubResultStatus.BadRequest, ex.Message);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unauthorized in {Method}", context.HubMethodName);
|
||||
return CreateHubErrorResult(context, HubResultStatus.Unauthorized, ex.Message);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Not found in {Method}", context.HubMethodName);
|
||||
return CreateHubErrorResult(context, HubResultStatus.NotFound, ex.Message);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid operation in {Method}", context.HubMethodName);
|
||||
return CreateHubErrorResult(context, HubResultStatus.Conflict, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled error in {Method}", context.HubMethodName);
|
||||
return CreateHubErrorResult(context, HubResultStatus.ServerError, "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
private static object CreateHubErrorResult(HubInvocationContext context, HubResultStatus status, string message)
|
||||
{
|
||||
var returnType = context.HubMethod.ReturnType;
|
||||
|
||||
// Поддержка Task<HubResult<T>>
|
||||
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
|
||||
{
|
||||
var resultType = returnType.GenericTypeArguments[0];
|
||||
|
||||
if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(HubResult<>))
|
||||
{
|
||||
var hubResultType = resultType;
|
||||
var errorResult = Activator.CreateInstance(hubResultType)!;
|
||||
|
||||
hubResultType.GetProperty(nameof(HubResult<object>.Status))!
|
||||
.SetValue(errorResult, status);
|
||||
hubResultType.GetProperty(nameof(HubResult<object>.ErrorMessage))!
|
||||
.SetValue(errorResult, message);
|
||||
|
||||
return Task.FromResult(errorResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Ничего не возвращаем, если не поддерживается
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,5 @@
|
||||
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\Friends\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
+77
-75
@@ -16,14 +16,14 @@ public class ChatsHub : Hub
|
||||
private readonly ILogger<ChatsHub> _logger;
|
||||
private readonly IMessageService _messageService;
|
||||
private readonly IUserGroupsService _userService;
|
||||
|
||||
|
||||
public ChatsHub(ILogger<ChatsHub> logger, IMessageService messageService, IUserGroupsService userService)
|
||||
{
|
||||
_logger = logger;
|
||||
_messageService = messageService;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
@@ -36,28 +36,34 @@ public class ChatsHub : Hub
|
||||
|
||||
// Add user to their own group (for private messages and notifications)
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
|
||||
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group", userId, Context.ConnectionId);
|
||||
|
||||
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group",
|
||||
userId, Context.ConnectionId);
|
||||
|
||||
var userGroups = await _userService.GetUserGroupsAsync(userId);
|
||||
foreach (var group in userGroups)
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"group_{group.Id}");
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"group_{group.Id}");
|
||||
}
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
var userId = GetUserId(suppressException: true); // Suppress exception if userID is not found (e.g. connection aborted early)
|
||||
var userId =
|
||||
GetUserId(suppressException: true); // Suppress exception if userID is not found (e.g. connection aborted early)
|
||||
if (userId != Guid.Empty)
|
||||
{
|
||||
// Remove user from their own group
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
|
||||
_logger.LogInformation("User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId, Context.ConnectionId);
|
||||
_logger.LogInformation(
|
||||
"User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId,
|
||||
Context.ConnectionId);
|
||||
|
||||
|
||||
var userGroups = await _userService.GetUserGroupsAsync(userId); // This might be problematic if the service relies on the user being connected
|
||||
var userGroups =
|
||||
await _userService
|
||||
.GetUserGroupsAsync(
|
||||
userId); // This might be problematic if the service relies on the user being connected
|
||||
foreach (var group in userGroups)
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group_{group.Id}");
|
||||
@@ -65,11 +71,15 @@ public class ChatsHub : Hub
|
||||
}
|
||||
else if (exception != null)
|
||||
{
|
||||
_logger.LogWarning(exception, "User disconnected with an exception and invalid UserID claim. ConnectionId: {ConnectionId}", Context.ConnectionId);
|
||||
_logger.LogWarning(exception,
|
||||
"User disconnected with an exception and invalid UserID claim. ConnectionId: {ConnectionId}",
|
||||
Context.ConnectionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("User disconnected with no exception and invalid UserID claim. ConnectionId: {ConnectionId}", Context.ConnectionId);
|
||||
_logger.LogInformation(
|
||||
"User disconnected with no exception and invalid UserID claim. ConnectionId: {ConnectionId}",
|
||||
Context.ConnectionId);
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
@@ -77,88 +87,78 @@ public class ChatsHub : Hub
|
||||
|
||||
public async Task Send(MessageRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.EncryptedContent) && (request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
||||
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
|
||||
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
||||
{
|
||||
_logger.LogWarning("Empty message (no content and no attachments) received from user {UserId}", GetUserId());
|
||||
_logger.LogWarning("Empty message (no content and no attachments) received from user {UserId}",
|
||||
GetUserId());
|
||||
throw new ArgumentException("Message cannot be empty (must have content or attachments).");
|
||||
}
|
||||
|
||||
var senderId = GetUserId();
|
||||
_logger.LogInformation("Message send initiated by {SenderId} to {RecipientId} of type {RecipientType}", senderId, request.RecipientId, request.RecipientType);
|
||||
_logger.LogInformation("Message send initiated by {SenderId} to {RecipientId} of type {RecipientType}",
|
||||
senderId, request.RecipientId, request.RecipientType);
|
||||
|
||||
var sendMessageParams = new SendMessage(
|
||||
EncryptContent: request.EncryptedContent,
|
||||
ReplyToMessageId: request.ReplyToMessageId,
|
||||
FromUserId: senderId,
|
||||
RecipientId: request.RecipientId,
|
||||
RecipientType: request.RecipientType,
|
||||
SendAt: DateTime.UtcNow,
|
||||
Media: request.MediaAttachments?.Select(f => new SendMedia(
|
||||
f.MediaId, f.EncryptedKey)) ?? Array.Empty<SendMedia>()
|
||||
);
|
||||
var sendMessageParams = new SendMessage(EncryptContent: request.EncryptedContent,
|
||||
ReplyToMessageId: request.ReplyToMessageId, FromUserId: senderId, RecipientId: request.RecipientId,
|
||||
RecipientType: request.RecipientType, SendAt: DateTime.UtcNow,
|
||||
Media: request.MediaAttachments?.Select(f => new SendMedia(f.MediaId, f.EncryptedKey)) ??
|
||||
Array.Empty<SendMedia>());
|
||||
|
||||
try
|
||||
var result = await _messageService.SendMessageAsync(sendMessageParams);
|
||||
|
||||
if (!result.IsSuccess || result.Message.Id == Guid.Empty)
|
||||
{
|
||||
var result = await _messageService.SendMessageAsync(sendMessageParams);
|
||||
|
||||
if (!result.IsSuccess || result.Message.Id == Guid.Empty)
|
||||
{
|
||||
_logger.LogError(result.Exception, "Failed to send message from {SenderId} to {RecipientId}. Error: {ErrorMessage}", senderId, request.RecipientId, result.Exception?.Message ?? "Unknown error");
|
||||
if (result.Exception != null) throw result.Exception;
|
||||
throw new HubException("Failed to send message due to an internal error.");
|
||||
}
|
||||
|
||||
var messageResponse = new UserMessageResponse // Assuming a response DTO
|
||||
{
|
||||
MessageId = result.Message.Id,
|
||||
SenderId = result.Message.SenderId,
|
||||
RecipientId = result.Message.RecipientId,
|
||||
RecipientType = result.Message.RecipientType,
|
||||
EncryptedContent = result.Message.EncryptedContent,
|
||||
SentAt = result.Message.SentAt,
|
||||
IsEdited = false,
|
||||
MediaAttachments = result.Message.MediaAttachments
|
||||
.Select(m => m.MediaFile).ToList(),
|
||||
ReplyToMessageId = request.ReplyToMessageId
|
||||
};
|
||||
|
||||
// Notify recipient (user or group)
|
||||
if (request.RecipientType == RecipientType.User)
|
||||
{
|
||||
// Send to the recipient's personal group
|
||||
await Clients.Group(request.RecipientId.ToString()).SendAsync("ReceiveMessage", messageResponse);
|
||||
}
|
||||
else if (request.RecipientType == RecipientType.Group)
|
||||
{
|
||||
// Send to all members of the group, including the sender if they are part of the group via a different connection
|
||||
await Clients.Group($"group_{request.RecipientId}").SendAsync("ReceiveMessage", messageResponse);
|
||||
}
|
||||
|
||||
// Notify sender (confirmation) on their connection
|
||||
await Clients.Caller.SendAsync("MessageSent", messageResponse); // Or use "ReceiveMessage" if the sender should also just get it like anyone else
|
||||
|
||||
_logger.LogInformation("Message {MessageId} sent successfully from {SenderId} to {RecipientId} ({RecipientType})", result.Message.Id, senderId, request.RecipientId, request.RecipientType);
|
||||
_logger.LogError(result.Exception,
|
||||
"Failed to send message from {SenderId} to {RecipientId}. Error: {ErrorMessage}", senderId,
|
||||
request.RecipientId, result.Exception?.Message ?? "Unknown error");
|
||||
if (result.Exception != null)
|
||||
throw result.Exception;
|
||||
throw new HubException("Failed to send message due to an internal error.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
var messageResponse = new UserMessageResponse // Assuming a response DTO
|
||||
{
|
||||
_logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId);
|
||||
// Consider sending a specific error message to the caller instead of a generic HubException or rethrowing.
|
||||
// For example: await Clients.Caller.SendAsync("SendMessageFailed", new { Error = ex.Message });
|
||||
throw new HubException("An error occurred while sending the message.", ex);
|
||||
MessageId = result.Message.Id,
|
||||
SenderId = result.Message.SenderId,
|
||||
RecipientId = result.Message.RecipientId,
|
||||
RecipientType = result.Message.RecipientType,
|
||||
EncryptedContent = result.Message.EncryptedContent,
|
||||
SentAt = result.Message.SentAt,
|
||||
IsEdited = false,
|
||||
MediaAttachments = result.Message.MediaAttachments.Select(m => m.MediaFile).ToList(),
|
||||
ReplyToMessageId = request.ReplyToMessageId
|
||||
};
|
||||
|
||||
// Notify recipient (user or group)
|
||||
if (request.RecipientType == RecipientType.User)
|
||||
{
|
||||
// Send to the recipient's personal group
|
||||
await Clients.Group(request.RecipientId.ToString()).SendAsync("ReceiveMessage", messageResponse);
|
||||
}
|
||||
else if (request.RecipientType == RecipientType.Group)
|
||||
{
|
||||
// Send to all members of the group, including the sender if they are part of the group via a different connection
|
||||
await Clients.Group($"group_{request.RecipientId}").SendAsync("ReceiveMessage", messageResponse);
|
||||
}
|
||||
|
||||
// Notify sender (confirmation) on their connection
|
||||
await Clients.Caller.SendAsync("MessageSent",
|
||||
messageResponse); // Or use "ReceiveMessage" if the sender should also just get it like anyone else
|
||||
|
||||
_logger.LogInformation(
|
||||
"Message {MessageId} sent successfully from {SenderId} to {RecipientId} ({RecipientType})",
|
||||
result.Message.Id, senderId, request.RecipientId, request.RecipientType);
|
||||
}
|
||||
|
||||
public async Task Remove(RemoveMessageRequest request)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task Edit(EditMessageRequest request)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Guid GetUserId(bool suppressException = false)
|
||||
{
|
||||
var userIdClaim = Context.User?.FindFirst("userId")?.Value;
|
||||
@@ -169,8 +169,10 @@ public class ChatsHub : Hub
|
||||
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
|
||||
throw new UnauthorizedAccessException("userID claim is missing or invalid.");
|
||||
}
|
||||
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,113 @@
|
||||
using Govor.Application.Exceptions.FriendsService;
|
||||
using Govor.Application.Interfaces.Friends;
|
||||
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
||||
using Govor.Contracts.Responses.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Govor.API.Hubs;
|
||||
|
||||
public class FriendsHub : Hub
|
||||
{
|
||||
private readonly IFriendRequestService _friendRequestService;
|
||||
private readonly ILogger<FriendsHub> _logger;
|
||||
private readonly IFriendRequestCommandService _friendRequestService;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public FriendsHub(IFriendRequestService friendRequestService, ICurrentUserService currentUserService)
|
||||
public FriendsHub(IFriendRequestCommandService friendRequestService, ICurrentUserService currentUserService, ILogger<FriendsHub> logger)
|
||||
{
|
||||
_friendRequestService = friendRequestService;
|
||||
_currentUserService = currentUserService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendRequest(Guid targetUserId)
|
||||
public async Task<HubResult<object>> SendRequest(Guid targetUserId)
|
||||
{
|
||||
var userId = _currentUserService.GetCurrentUserId();
|
||||
await _friendRequestService.SendFriendRequestAsync(userId, targetUserId);
|
||||
await Clients.User(targetUserId.ToString()).SendAsync("FriendRequestReceived", userId);
|
||||
try
|
||||
{
|
||||
var userId = _currentUserService.GetCurrentUserId();
|
||||
await _friendRequestService.SendAsync(userId, targetUserId);
|
||||
await Clients.User(targetUserId.ToString())
|
||||
.SendAsync("FriendRequestReceived", userId);
|
||||
|
||||
_logger.LogInformation($"Friend request received for user {targetUserId} from {userId}.");
|
||||
return HubResult<object>.Created();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return HubResult<object>.BadRequest(ex.Message);
|
||||
}
|
||||
catch (RequestAlreadySentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return HubResult<object>.Conflict(ex.Message);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return HubResult<object>.Unauthorized(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, ex.Message);
|
||||
return HubResult<object>.Error("Unexpected error! Please try later!");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AcceptRequest(Guid friendshipId)
|
||||
public async Task<HubResult<object>> AcceptRequest(Guid friendshipId)
|
||||
{
|
||||
var userId = _currentUserService.GetCurrentUserId();
|
||||
await _friendRequestService.AcceptFriendRequestAsync(friendshipId, userId);
|
||||
await Clients.User(userId.ToString()).SendAsync("FriendRequestAccepted", friendshipId);
|
||||
try
|
||||
{
|
||||
var userId = _currentUserService.GetCurrentUserId();
|
||||
await _friendRequestService.AcceptAsync(friendshipId, userId);
|
||||
await Clients.User(userId.ToString())
|
||||
.SendAsync("FriendRequestAccepted", friendshipId);
|
||||
|
||||
_logger.LogInformation($"Friend request accepted for user {userId} from {userId}.");
|
||||
return HubResult<object>.Ok();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return HubResult<object>.BadRequest(ex.Message);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return HubResult<object>.Unauthorized(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, ex.Message);
|
||||
return HubResult<object>.Error("Unexpected error! Please try later!");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RejectRequest(Guid friendshipId)
|
||||
public async Task<HubResult<object>> RejectRequest(Guid friendshipId)
|
||||
{
|
||||
var userId = _currentUserService.GetCurrentUserId();
|
||||
await _friendRequestService.RejectFriendRequestAsync(friendshipId, userId);
|
||||
await Clients.User(userId.ToString()).SendAsync("FriendRequestRejected", friendshipId);
|
||||
try
|
||||
{
|
||||
var userId = _currentUserService.GetCurrentUserId();
|
||||
await _friendRequestService.RejectAsync(friendshipId, userId);
|
||||
await Clients.User(userId.ToString())
|
||||
.SendAsync("FriendRequestRejected", friendshipId);
|
||||
|
||||
_logger.LogInformation($"Friend request rejected for user {userId} from {userId}.");
|
||||
return HubResult<object>.Ok();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return HubResult<object>.BadRequest(ex.Message);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return HubResult<object>.Unauthorized(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, ex.Message);
|
||||
return HubResult<object>.Error("Unexpected error! Please try later!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ builder.Services.AddCors(options =>
|
||||
builder.Services.Configure<JwtOption>(configuration.GetSection(nameof(JwtOption)));
|
||||
|
||||
// Add services
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddSignalRConf();// signalR
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||
|
||||
Reference in New Issue
Block a user