yep
This commit is contained in:
@@ -77,6 +77,11 @@ public class SessionsController : ControllerBase
|
||||
return Ok(await this.queryRespository.ReadSessionPlayerCount(this.User.GetShogiUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch the session and latest board state. Also subscribe the user to socket events for this session.
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<ReadSessionResponse>> GetSession(string name)
|
||||
{
|
||||
@@ -113,6 +118,7 @@ public class SessionsController : ControllerBase
|
||||
session.AddPlayer2(User.GetShogiUserId());
|
||||
|
||||
await sessionRepository.SetPlayer2(name, User.GetShogiUserId());
|
||||
await communicationManager.BroadcastToAll(new SessionJoinedByPlayerSocketMessage());
|
||||
return this.Ok();
|
||||
}
|
||||
return this.Conflict("This game already has two players.");
|
||||
@@ -144,6 +150,8 @@ public class SessionsController : ControllerBase
|
||||
return this.Conflict(e.Message);
|
||||
}
|
||||
await sessionRepository.CreateMove(sessionName, command);
|
||||
|
||||
// Send socket message to both players so their clients know that new board state is available.
|
||||
await communicationManager.BroadcastToPlayers(
|
||||
new PlayerHasMovedMessage
|
||||
{
|
||||
|
||||
@@ -14,60 +14,68 @@ namespace Shogi.Api.Controllers;
|
||||
[Authorize]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly ISocketTokenCache tokenCache;
|
||||
private readonly ISocketConnectionManager connectionManager;
|
||||
private readonly IUserRepository userRepository;
|
||||
private readonly IShogiUserClaimsTransformer claimsTransformation;
|
||||
private readonly AuthenticationProperties authenticationProps;
|
||||
private readonly ISocketTokenCache tokenCache;
|
||||
private readonly ISocketConnectionManager connectionManager;
|
||||
private readonly IUserRepository userRepository;
|
||||
private readonly IShogiUserClaimsTransformer claimsTransformation;
|
||||
private readonly AuthenticationProperties authenticationProps;
|
||||
|
||||
public UserController(
|
||||
ILogger<UserController> logger,
|
||||
ISocketTokenCache tokenCache,
|
||||
ISocketConnectionManager connectionManager,
|
||||
IUserRepository userRepository,
|
||||
IShogiUserClaimsTransformer claimsTransformation)
|
||||
{
|
||||
this.tokenCache = tokenCache;
|
||||
this.connectionManager = connectionManager;
|
||||
this.userRepository = userRepository;
|
||||
this.claimsTransformation = claimsTransformation;
|
||||
authenticationProps = new AuthenticationProperties
|
||||
{
|
||||
AllowRefresh = true,
|
||||
IsPersistent = true
|
||||
};
|
||||
}
|
||||
public UserController(
|
||||
ILogger<UserController> logger,
|
||||
ISocketTokenCache tokenCache,
|
||||
ISocketConnectionManager connectionManager,
|
||||
IUserRepository userRepository,
|
||||
IShogiUserClaimsTransformer claimsTransformation)
|
||||
{
|
||||
this.tokenCache = tokenCache;
|
||||
this.connectionManager = connectionManager;
|
||||
this.userRepository = userRepository;
|
||||
this.claimsTransformation = claimsTransformation;
|
||||
authenticationProps = new AuthenticationProperties
|
||||
{
|
||||
AllowRefresh = true,
|
||||
IsPersistent = true
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("Token")]
|
||||
public ActionResult<CreateTokenResponse> GetWebSocketToken()
|
||||
{
|
||||
var userId = User.GetShogiUserId();
|
||||
var displayName = User.GetShogiUserDisplayname();
|
||||
[HttpGet("Token")]
|
||||
public ActionResult<CreateTokenResponse> GetWebSocketToken()
|
||||
{
|
||||
var userId = User.GetShogiUserId();
|
||||
var displayName = User.GetShogiUserDisplayname();
|
||||
|
||||
var token = tokenCache.GenerateToken(userId);
|
||||
return new CreateTokenResponse
|
||||
{
|
||||
DisplayName = displayName,
|
||||
OneTimeToken = token,
|
||||
UserId = userId
|
||||
};
|
||||
}
|
||||
var token = tokenCache.GenerateToken(userId);
|
||||
return new CreateTokenResponse
|
||||
{
|
||||
DisplayName = displayName,
|
||||
OneTimeToken = token,
|
||||
UserId = userId
|
||||
};
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("LoginAsGuest")]
|
||||
public async Task<IActionResult> GuestLogin()
|
||||
{
|
||||
var principal = await this.claimsTransformation.CreateClaimsFromGuestPrincipal(User);
|
||||
if (principal != null)
|
||||
{
|
||||
await HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
principal,
|
||||
authenticationProps
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="returnUrl">Used by cookie authentication.</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("LoginAsGuest")]
|
||||
public async Task<IActionResult> GuestLogin([FromQuery] string returnUrl)
|
||||
{
|
||||
var principal = await this.claimsTransformation.CreateClaimsFromGuestPrincipal(User);
|
||||
if (principal != null)
|
||||
{
|
||||
await HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
principal,
|
||||
authenticationProps
|
||||
);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPut("GuestLogout")]
|
||||
public async Task<IActionResult> GuestLogout()
|
||||
|
||||
@@ -9,91 +9,96 @@ using System.Text.Json;
|
||||
|
||||
namespace Shogi.Api.Services
|
||||
{
|
||||
public interface ISocketService
|
||||
{
|
||||
Task HandleSocketRequest(HttpContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages.
|
||||
/// </summary>
|
||||
public class SocketService : ISocketService
|
||||
{
|
||||
private readonly ILogger<SocketService> logger;
|
||||
private readonly ISocketConnectionManager communicationManager;
|
||||
private readonly ISocketTokenCache tokenManager;
|
||||
|
||||
public SocketService(
|
||||
ILogger<SocketService> logger,
|
||||
ISocketConnectionManager communicationManager,
|
||||
ISocketTokenCache tokenManager) : base()
|
||||
public interface ISocketService
|
||||
{
|
||||
this.logger = logger;
|
||||
this.communicationManager = communicationManager;
|
||||
this.tokenManager = tokenManager;
|
||||
Task HandleSocketRequest(HttpContext context);
|
||||
}
|
||||
|
||||
public async Task HandleSocketRequest(HttpContext context)
|
||||
/// <summary>
|
||||
/// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages.
|
||||
/// </summary>
|
||||
public class SocketService : ISocketService
|
||||
{
|
||||
if (!context.Request.Query.Keys.Contains("token"))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
var token = Guid.Parse(context.Request.Query["token"][0]);
|
||||
var userName = tokenManager.GetUsername(token);
|
||||
private readonly ILogger<SocketService> logger;
|
||||
private readonly ISocketConnectionManager communicationManager;
|
||||
private readonly ISocketTokenCache tokenManager;
|
||||
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
public SocketService(
|
||||
ILogger<SocketService> logger,
|
||||
ISocketConnectionManager communicationManager,
|
||||
ISocketTokenCache tokenManager) : base()
|
||||
{
|
||||
this.logger = logger;
|
||||
this.communicationManager = communicationManager;
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
communicationManager.Subscribe(socket, userName);
|
||||
while (socket.State == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
public async Task HandleSocketRequest(HttpContext context)
|
||||
{
|
||||
var message = await socket.ReceiveTextAsync();
|
||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||
logger.LogInformation("Request \n{0}\n", message);
|
||||
var request = JsonSerializer.Deserialize<ISocketRequest>(message);
|
||||
if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action))
|
||||
{
|
||||
await socket.SendTextAsync("Error: Action not recognized.");
|
||||
continue;
|
||||
}
|
||||
switch (request.Action)
|
||||
{
|
||||
default:
|
||||
await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work.");
|
||||
break;
|
||||
}
|
||||
if (!context.Request.Query.Keys.Contains("token"))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
var token = Guid.Parse(context.Request.Query["token"][0] ?? throw new InvalidOperationException("Token expected during socket connection request, but was not sent."));
|
||||
var userName = tokenManager.GetUsername(token);
|
||||
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
communicationManager.Subscribe(socket, userName);
|
||||
// TODO: I probably don't need this while-loop anymore? Perhaps unsubscribe when a disconnect is detected instead.
|
||||
while (socket.State.HasFlag(WebSocketState.Open))
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = await socket.ReceiveTextAsync();
|
||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||
logger.LogInformation("Request \n{0}\n", message);
|
||||
var request = JsonSerializer.Deserialize<ISocketRequest>(message);
|
||||
if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action))
|
||||
{
|
||||
await socket.SendTextAsync("Error: Action not recognized.");
|
||||
continue;
|
||||
}
|
||||
switch (request.Action)
|
||||
{
|
||||
default:
|
||||
await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogError(ex.Message);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
|
||||
logger.LogInformation("Probably tried writing to a closed socket.");
|
||||
logger.LogError(ex.Message);
|
||||
}
|
||||
communicationManager.Unsubscribe(userName);
|
||||
|
||||
if (!socket.State.HasFlag(WebSocketState.Closed) && !socket.State.HasFlag(WebSocketState.Aborted))
|
||||
{
|
||||
try
|
||||
{
|
||||
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure,
|
||||
"Socket closed",
|
||||
CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Ignored exception during socket closing. {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogError(ex.Message);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
|
||||
logger.LogInformation("Probably tried writing to a closed socket.");
|
||||
logger.LogError(ex.Message);
|
||||
}
|
||||
communicationManager.Unsubscribe(userName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateRequestAndReplyIfInvalid<TRequest>(WebSocket socket, IValidator<TRequest> validator, TRequest request)
|
||||
{
|
||||
var results = validator.Validate(request);
|
||||
if (!results.IsValid)
|
||||
{
|
||||
var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage));
|
||||
await socket.SendTextAsync(errors);
|
||||
}
|
||||
return results.IsValid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user