This commit is contained in:
2023-01-28 13:21:47 -06:00
parent 11b387b928
commit 8a25c0ed35
26 changed files with 443 additions and 359 deletions

View File

@@ -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
{

View File

@@ -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()

View File

@@ -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;
}
}
}