massive checkpoint
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
17
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs
Normal file
17
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||||
|
{
|
||||||
|
public class GetGameResponse
|
||||||
|
{
|
||||||
|
public Game Game { get; set; }
|
||||||
|
public WhichPlayer PlayerPerspective { get; set; }
|
||||||
|
public BoardState BoardState { get; set; }
|
||||||
|
public IList<Move> MoveHistory { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,6 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|||||||
{
|
{
|
||||||
public class PostMove
|
public class PostMove
|
||||||
{
|
{
|
||||||
[Required]
|
|
||||||
public string GameName { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public Move Move { get; set; }
|
public Move Move { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
public class PostSession
|
public class PostSession
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Player1 { get; set; }
|
|
||||||
public string Player2 { get; set; }
|
|
||||||
public bool IsPrivate { get; set; }
|
public bool IsPrivate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||||
{
|
{
|
||||||
public class CreateGameRequest : IRequest
|
|
||||||
{
|
|
||||||
public ClientAction Action { get; set; }
|
|
||||||
public string GameName { get; set; } = string.Empty;
|
|
||||||
public bool IsPrivate { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CreateGameResponse : IResponse
|
public class CreateGameResponse : IResponse
|
||||||
{
|
{
|
||||||
public string Action { get; }
|
public string Action { get; }
|
||||||
public string Error { get; set; }
|
|
||||||
public Game Game { get; set; }
|
public Game Game { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The player who created the game.
|
||||||
|
/// </summary>
|
||||||
public string PlayerName { get; set; }
|
public string PlayerName { get; set; }
|
||||||
|
|
||||||
public CreateGameResponse()
|
public CreateGameResponse()
|
||||||
{
|
{
|
||||||
Action = ClientAction.CreateGame.ToString();
|
Action = ClientAction.CreateGame.ToString();
|
||||||
Error = string.Empty;
|
|
||||||
Game = new Game();
|
|
||||||
PlayerName = string.Empty;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,5 @@
|
|||||||
public interface IResponse
|
public interface IResponse
|
||||||
{
|
{
|
||||||
string Action { get; }
|
string Action { get; }
|
||||||
string Error { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|
||||||
{
|
|
||||||
public class ListGamesRequest : IRequest
|
|
||||||
{
|
|
||||||
public ClientAction Action { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ListGamesResponse : IResponse
|
|
||||||
{
|
|
||||||
public string Action { get; }
|
|
||||||
public string Error { get; set; }
|
|
||||||
public IReadOnlyList<Game> Games { get; set; }
|
|
||||||
|
|
||||||
public ListGamesResponse()
|
|
||||||
{
|
|
||||||
Action = ClientAction.ListGames.ToString();
|
|
||||||
Error = "";
|
|
||||||
Games = new Collection<Game>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|
||||||
{
|
|
||||||
public class LoadGameRequest : IRequest
|
|
||||||
{
|
|
||||||
public ClientAction Action { get; set; }
|
|
||||||
public string GameName { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoadGameResponse : IResponse
|
|
||||||
{
|
|
||||||
public string Action { get; }
|
|
||||||
public Game Game { get; set; }
|
|
||||||
public WhichPlayer PlayerPerspective { get; set; }
|
|
||||||
public BoardState BoardState { get; set; }
|
|
||||||
public IList<Move> MoveHistory { get; set; }
|
|
||||||
public string Error { get; set; }
|
|
||||||
|
|
||||||
public LoadGameResponse()
|
|
||||||
{
|
|
||||||
Action = ClientAction.LoadGame.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,19 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||||
{
|
{
|
||||||
public class MoveRequest : IRequest
|
|
||||||
{
|
|
||||||
public ClientAction Action { get; set; }
|
|
||||||
public string GameName { get; set; } = string.Empty;
|
|
||||||
public Move Move { get; set; } = new Move();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MoveResponse : IResponse
|
public class MoveResponse : IResponse
|
||||||
{
|
{
|
||||||
public string Action { get; }
|
public string Action { get; protected set; }
|
||||||
public string Error { get; set; }
|
public Game Game { get; set; }
|
||||||
public string GameName { get; set; }
|
public WhichPlayer PlayerPerspective { get; set; }
|
||||||
public string PlayerName { get; set; }
|
public BoardState BoardState { get; set; }
|
||||||
public Move Move { get; set; }
|
public IList<Move> MoveHistory { get; set; }
|
||||||
|
|
||||||
public MoveResponse()
|
public MoveResponse()
|
||||||
{
|
{
|
||||||
Action = ClientAction.Move.ToString();
|
Action = ClientAction.Move.ToString();
|
||||||
Error = string.Empty;
|
|
||||||
GameName = string.Empty;
|
|
||||||
PlayerName = string.Empty;
|
|
||||||
Move = new Move();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
{
|
{
|
||||||
public enum ClientAction
|
public enum ClientAction
|
||||||
{
|
{
|
||||||
ListGames,
|
|
||||||
CreateGame,
|
CreateGame,
|
||||||
JoinGame,
|
JoinGame,
|
||||||
JoinByCode,
|
JoinByCode,
|
||||||
LoadGame,
|
|
||||||
Move
|
Move
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
|
using Gameboard.ShogiUI.Sockets.Managers;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||||
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System;
|
using System;
|
||||||
@@ -10,16 +12,15 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
|
[Authorize(Roles = "Shogi")]
|
||||||
public class GameController : ControllerBase
|
public class GameController : ControllerBase
|
||||||
{
|
{
|
||||||
private static readonly string UsernameClaim = "preferred_username";
|
|
||||||
private readonly IGameboardManager gameboardManager;
|
private readonly IGameboardManager gameboardManager;
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
private readonly IGameboardRepository gameboardRepository;
|
||||||
private readonly ISocketConnectionManager communicationManager;
|
private readonly ISocketConnectionManager communicationManager;
|
||||||
private string? JwtUserName => HttpContext.User.Claims.FirstOrDefault(c => c.Type == UsernameClaim)?.Value;
|
|
||||||
|
|
||||||
public GameController(
|
public GameController(
|
||||||
IGameboardRepository repository,
|
IGameboardRepository repository,
|
||||||
@@ -68,22 +69,12 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
[HttpPost("{gameName}/Move")]
|
[HttpPost("{gameName}/Move")]
|
||||||
public async Task<IActionResult> PostMove([FromRoute] string gameName, [FromBody] PostMove request)
|
public async Task<IActionResult> PostMove([FromRoute] string gameName, [FromBody] PostMove request)
|
||||||
{
|
{
|
||||||
Models.User? user = null;
|
var user = await gameboardManager.ReadUser(User);
|
||||||
if (Request.Cookies.ContainsKey(SocketController.WebSessionKey))
|
var session = await gameboardRepository.ReadSession(gameName);
|
||||||
{
|
|
||||||
var webSessionId = Guid.Parse(Request.Cookies[SocketController.WebSessionKey]!);
|
|
||||||
user = await gameboardManager.ReadUser(webSessionId);
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(JwtUserName))
|
|
||||||
{
|
|
||||||
user = await gameboardManager.ReadUser(JwtUserName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await gameboardManager.ReadSession(gameName);
|
|
||||||
|
|
||||||
if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name))
|
if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name))
|
||||||
{
|
{
|
||||||
throw new UnauthorizedAccessException("You are not seated at this game.");
|
throw new UnauthorizedAccessException("User is not seated at this game.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var move = request.Move;
|
var move = request.Move;
|
||||||
@@ -94,13 +85,19 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
|
|
||||||
if (moveSuccess)
|
if (moveSuccess)
|
||||||
{
|
{
|
||||||
|
var createSuccess = await gameboardRepository.CreateBoardState(session);
|
||||||
|
if (!createSuccess)
|
||||||
|
{
|
||||||
|
throw new ApplicationException("Unable to persist board state.");
|
||||||
|
}
|
||||||
await communicationManager.BroadcastToPlayers(new MoveResponse
|
await communicationManager.BroadcastToPlayers(new MoveResponse
|
||||||
{
|
{
|
||||||
GameName = session.Name,
|
BoardState = session.Shogi.ToServiceModel(),
|
||||||
PlayerName = user.Name,
|
Game = session.ToServiceModel(),
|
||||||
Move = moveModel.ToServiceModel()
|
MoveHistory = session.Shogi.MoveHistory.Select(h => h.ToServiceModel()).ToList(),
|
||||||
|
PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2
|
||||||
}, session.Player1, session.Player2);
|
}, session.Player1, session.Player2);
|
||||||
return Created(string.Empty, null);
|
return Ok();
|
||||||
}
|
}
|
||||||
throw new InvalidOperationException("Illegal move.");
|
throw new InvalidOperationException("Illegal move.");
|
||||||
}
|
}
|
||||||
@@ -124,5 +121,56 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
// }
|
// }
|
||||||
// return new ConflictResult();
|
// return new ConflictResult();
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||||
|
{
|
||||||
|
var user = await gameboardManager.ReadUser(User);
|
||||||
|
var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!.Name);
|
||||||
|
var success = await gameboardRepository.CreateSession(session);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
await communicationManager.BroadcastToAll(new CreateGameResponse
|
||||||
|
{
|
||||||
|
Game = session.ToServiceModel(),
|
||||||
|
PlayerName = user.Name
|
||||||
|
});
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
return Conflict();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the board session and subscribes the caller to socket events for that session.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{gameName}")]
|
||||||
|
public async Task<IActionResult> GetSession([FromRoute] string gameName)
|
||||||
|
{
|
||||||
|
var user = await gameboardManager.ReadUser(User);
|
||||||
|
var session = await gameboardRepository.ReadSession(gameName);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
communicationManager.SubscribeToGame(session, user!.Name);
|
||||||
|
var response = new GetGameResponse()
|
||||||
|
{
|
||||||
|
Game = new Models.SessionMetadata(session).ToServiceModel(),
|
||||||
|
BoardState = session.Shogi.ToServiceModel(),
|
||||||
|
MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(),
|
||||||
|
PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2
|
||||||
|
};
|
||||||
|
return new JsonResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetSessions()
|
||||||
|
{
|
||||||
|
var sessions = await gameboardRepository.ReadSessionMetadatas();
|
||||||
|
return new JsonResult(sessions.Select(s => s.ToServiceModel()).ToList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,113 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
|
using Gameboard.ShogiUI.Sockets.Managers;
|
||||||
|
using Gameboard.ShogiUI.Sockets.Models;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
|
||||||
[Route("[controller]")]
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
[Authorize(Roles = "Shogi")]
|
||||||
public class SocketController : ControllerBase
|
public class SocketController : ControllerBase
|
||||||
{
|
{
|
||||||
public static readonly string WebSessionKey = "session-id";
|
|
||||||
private readonly ILogger<SocketController> logger;
|
private readonly ILogger<SocketController> logger;
|
||||||
private readonly ISocketTokenManager tokenManager;
|
private readonly ISocketTokenCache tokenCache;
|
||||||
private readonly IGameboardManager gameboardManager;
|
private readonly IGameboardManager gameboardManager;
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
private readonly IGameboardRepository gameboardRepository;
|
||||||
private readonly CookieOptions createSessionOptions;
|
private readonly AuthenticationProperties authenticationProps;
|
||||||
private readonly CookieOptions deleteSessionOptions;
|
|
||||||
|
|
||||||
public SocketController(
|
public SocketController(
|
||||||
ILogger<SocketController> logger,
|
ILogger<SocketController> logger,
|
||||||
ISocketTokenManager tokenManager,
|
ISocketTokenCache tokenCache,
|
||||||
IGameboardManager gameboardManager,
|
IGameboardManager gameboardManager,
|
||||||
IGameboardRepository gameboardRepository)
|
IGameboardRepository gameboardRepository)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.tokenManager = tokenManager;
|
this.tokenCache = tokenCache;
|
||||||
this.gameboardManager = gameboardManager;
|
this.gameboardManager = gameboardManager;
|
||||||
this.gameboardRepository = gameboardRepository;
|
this.gameboardRepository = gameboardRepository;
|
||||||
createSessionOptions = new CookieOptions
|
authenticationProps = new AuthenticationProperties
|
||||||
{
|
{
|
||||||
Secure = true,
|
AllowRefresh = true,
|
||||||
HttpOnly = true,
|
IsPersistent = true
|
||||||
SameSite = SameSiteMode.None,
|
|
||||||
Expires = DateTimeOffset.Now.AddYears(5)
|
|
||||||
};
|
};
|
||||||
deleteSessionOptions = new CookieOptions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("Yep")]
|
[HttpGet("GuestLogout")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public IActionResult Yep()
|
public async Task<IActionResult> GuestLogout()
|
||||||
{
|
{
|
||||||
deleteSessionOptions.Expires = DateTimeOffset.Now.AddDays(-1);
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
Response.Cookies.Append(WebSessionKey, "", deleteSessionOptions);
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("Token")]
|
[HttpGet("Token")]
|
||||||
public IActionResult GetToken()
|
public async Task<IActionResult> GetToken()
|
||||||
{
|
{
|
||||||
var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
|
var identityId = User.UserId();
|
||||||
var token = tokenManager.GenerateToken(userName);
|
if (string.IsNullOrWhiteSpace(identityId))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await gameboardManager.ReadUser(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
user = new User(identityId);
|
||||||
|
var success = await gameboardRepository.CreateUser(user);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = tokenCache.GenerateToken(user.Name);
|
||||||
return new JsonResult(new GetTokenResponse(token));
|
return new JsonResult(new GetTokenResponse(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a token for guest users to send when requesting a socket connection.
|
|
||||||
/// Sends a HttpOnly cookie to the client with which to identify guest users.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpGet("GuestToken")]
|
[HttpGet("GuestToken")]
|
||||||
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> GetGuestToken()
|
public async Task<IActionResult> GetGuestToken()
|
||||||
{
|
{
|
||||||
var cookies = Request.Cookies;
|
if (Guid.TryParse(User.UserId(), out Guid webSessionId))
|
||||||
var webSessionId = cookies.ContainsKey(WebSessionKey)
|
{
|
||||||
? Guid.Parse(cookies[WebSessionKey]!)
|
|
||||||
: Guid.NewGuid();
|
|
||||||
var webSessionIdAsString = webSessionId.ToString();
|
|
||||||
|
|
||||||
var user = await gameboardRepository.ReadGuestUser(webSessionId);
|
var user = await gameboardRepository.ReadGuestUser(webSessionId);
|
||||||
if (user == null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
var userName = await gameboardManager.CreateGuestUser(webSessionId);
|
var token = tokenCache.GenerateToken(webSessionId.ToString());
|
||||||
var token = tokenManager.GenerateToken(webSessionIdAsString);
|
|
||||||
Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
|
|
||||||
return new JsonResult(new GetGuestTokenResponse(userName, token));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var token = tokenManager.GenerateToken(webSessionIdAsString);
|
|
||||||
Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
|
|
||||||
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Setup a guest user.
|
||||||
|
var newSessionId = Guid.NewGuid();
|
||||||
|
var user = new User(Guid.NewGuid().ToString(), newSessionId);
|
||||||
|
if (await gameboardRepository.CreateUser(user))
|
||||||
|
{
|
||||||
|
var identity = user.CreateGuestUserIdentity();
|
||||||
|
await this.HttpContext.SignInAsync(
|
||||||
|
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
new ClaimsPrincipal(identity),
|
||||||
|
authenticationProps
|
||||||
|
);
|
||||||
|
|
||||||
|
var token = tokenCache.GenerateToken(newSessionId.ToString());
|
||||||
|
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
Normal file
18
Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Extensions
|
||||||
|
{
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static string? UserId(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsGuest(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
return self.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Guest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation" Version="10.3.0" />
|
<PackageReference Include="FluentValidation" Version="10.3.3" />
|
||||||
<PackageReference Include="IdentityModel" Version="5.0.0" />
|
<PackageReference Include="IdentityModel" Version="5.1.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="5.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="5.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.5.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.9" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.5.1" />
|
<PackageReference Include="Microsoft.Identity.Web" Version="1.16.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.16.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
|
<PackageReference Include="NSwag.AspNetCore" Version="13.13.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
|
||||||
{
|
|
||||||
public interface ICreateGameHandler
|
|
||||||
{
|
|
||||||
Task Handle(CreateGameRequest request, string userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This doesn't need to be a socket action.
|
|
||||||
// It can be an API route and still tell socket connections about the new session.
|
|
||||||
public class CreateGameHandler : ICreateGameHandler
|
|
||||||
{
|
|
||||||
private readonly IGameboardManager manager;
|
|
||||||
private readonly ISocketConnectionManager connectionManager;
|
|
||||||
|
|
||||||
public CreateGameHandler(
|
|
||||||
ISocketConnectionManager communicationManager,
|
|
||||||
IGameboardManager manager)
|
|
||||||
{
|
|
||||||
this.manager = manager;
|
|
||||||
this.connectionManager = communicationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(CreateGameRequest request, string userName)
|
|
||||||
{
|
|
||||||
var model = new SessionMetadata(request.GameName, request.IsPrivate, userName, null);
|
|
||||||
var success = await manager.CreateSession(model);
|
|
||||||
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
var error = new CreateGameResponse()
|
|
||||||
{
|
|
||||||
Error = "Unable to create game with this name."
|
|
||||||
};
|
|
||||||
await connectionManager.BroadcastToPlayers(error, userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = new CreateGameResponse()
|
|
||||||
{
|
|
||||||
PlayerName = userName,
|
|
||||||
Game = model.ToServiceModel()
|
|
||||||
};
|
|
||||||
|
|
||||||
var task = request.IsPrivate
|
|
||||||
? connectionManager.BroadcastToPlayers(response, userName)
|
|
||||||
: connectionManager.BroadcastToAll(response);
|
|
||||||
|
|
||||||
await task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
|
||||||
{
|
|
||||||
public interface IListGamesHandler
|
|
||||||
{
|
|
||||||
Task Handle(ListGamesRequest request, string userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ListGamesHandler : IListGamesHandler
|
|
||||||
{
|
|
||||||
private readonly ISocketConnectionManager communicationManager;
|
|
||||||
private readonly IGameboardRepository repository;
|
|
||||||
|
|
||||||
public ListGamesHandler(
|
|
||||||
ISocketConnectionManager communicationManager,
|
|
||||||
IGameboardRepository repository)
|
|
||||||
{
|
|
||||||
this.communicationManager = communicationManager;
|
|
||||||
this.repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(ListGamesRequest _, string userName)
|
|
||||||
{
|
|
||||||
var sessions = await repository.ReadSessionMetadatas();
|
|
||||||
var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList();
|
|
||||||
|
|
||||||
var response = new ListGamesResponse()
|
|
||||||
{
|
|
||||||
Games = games
|
|
||||||
};
|
|
||||||
|
|
||||||
await communicationManager.BroadcastToPlayers(response, userName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
|
||||||
{
|
|
||||||
public interface ILoadGameHandler
|
|
||||||
{
|
|
||||||
Task Handle(LoadGameRequest request, string userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subscribes a user to messages for a session and loads that session into the BoardManager for playing.
|
|
||||||
/// </summary>
|
|
||||||
public class LoadGameHandler : ILoadGameHandler
|
|
||||||
{
|
|
||||||
private readonly ILogger<LoadGameHandler> logger;
|
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
|
||||||
private readonly ISocketConnectionManager communicationManager;
|
|
||||||
|
|
||||||
public LoadGameHandler(
|
|
||||||
ILogger<LoadGameHandler> logger,
|
|
||||||
ISocketConnectionManager communicationManager,
|
|
||||||
IGameboardRepository gameboardRepository)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.gameboardRepository = gameboardRepository;
|
|
||||||
this.communicationManager = communicationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(LoadGameRequest request, string userName)
|
|
||||||
{
|
|
||||||
var sessionModel = await gameboardRepository.ReadSession(request.GameName);
|
|
||||||
if (sessionModel == null)
|
|
||||||
{
|
|
||||||
logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName);
|
|
||||||
var error = new LoadGameResponse() { Error = "Game not found." };
|
|
||||||
await communicationManager.BroadcastToPlayers(error, userName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
communicationManager.SubscribeToGame(sessionModel, userName);
|
|
||||||
|
|
||||||
var response = new LoadGameResponse()
|
|
||||||
{
|
|
||||||
Game = new SessionMetadata(sessionModel).ToServiceModel(),
|
|
||||||
BoardState = sessionModel.Shogi.ToServiceModel(),
|
|
||||||
MoveHistory = sessionModel.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList()
|
|
||||||
};
|
|
||||||
await communicationManager.BroadcastToPlayers(response, userName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
|
||||||
{
|
|
||||||
public interface IMoveHandler
|
|
||||||
{
|
|
||||||
Task Handle(MoveRequest request, string userName);
|
|
||||||
}
|
|
||||||
public class MoveHandler : IMoveHandler
|
|
||||||
{
|
|
||||||
private readonly IGameboardManager gameboardManager;
|
|
||||||
private readonly ISocketConnectionManager connectionManager;
|
|
||||||
public MoveHandler(
|
|
||||||
ISocketConnectionManager connectionManager,
|
|
||||||
IGameboardManager gameboardManager)
|
|
||||||
{
|
|
||||||
this.gameboardManager = gameboardManager;
|
|
||||||
this.connectionManager = connectionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(MoveRequest request, string userName)
|
|
||||||
{
|
|
||||||
Models.Move moveModel;
|
|
||||||
if (request.Move.PieceFromCaptured.HasValue)
|
|
||||||
{
|
|
||||||
moveModel = new Models.Move(request.Move.PieceFromCaptured.Value, request.Move.To);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
moveModel = new Models.Move(request.Move.From!, request.Move.To, request.Move.IsPromotion);
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await gameboardManager.ReadSession(request.GameName);
|
|
||||||
if (session != null)
|
|
||||||
{
|
|
||||||
var shogi = session.Shogi;
|
|
||||||
var moveSuccess = shogi.Move(moveModel);
|
|
||||||
if (moveSuccess)
|
|
||||||
{
|
|
||||||
await gameboardManager.CreateBoardState(session.Name, shogi);
|
|
||||||
var response = new MoveResponse()
|
|
||||||
{
|
|
||||||
GameName = request.GameName,
|
|
||||||
PlayerName = userName,
|
|
||||||
Move = moveModel.ToServiceModel()
|
|
||||||
};
|
|
||||||
await connectionManager.BroadcastToPlayers(response, session.Player1, session.Player2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var response = new MoveResponse()
|
|
||||||
{
|
|
||||||
Error = "Invalid move."
|
|
||||||
};
|
|
||||||
await connectionManager.BroadcastToPlayers(response, userName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,21 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Models;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
|
using Gameboard.ShogiUI.Sockets.Models;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers
|
namespace Gameboard.ShogiUI.Sockets.Managers
|
||||||
{
|
{
|
||||||
public interface IGameboardManager
|
public interface IGameboardManager
|
||||||
{
|
{
|
||||||
Task<string> CreateGuestUser(Guid webSessionId);
|
|
||||||
Task<bool> IsPlayer1(string sessionName, string playerName);
|
Task<bool> IsPlayer1(string sessionName, string playerName);
|
||||||
Task<bool> CreateSession(SessionMetadata session);
|
|
||||||
Task<Session?> ReadSession(string gameName);
|
|
||||||
Task<bool> UpdateSession(SessionMetadata session);
|
|
||||||
Task<bool> AssignPlayer2ToSession(string sessionName, string userName);
|
Task<bool> AssignPlayer2ToSession(string sessionName, string userName);
|
||||||
Task<bool> CreateBoardState(string sessionName, Shogi shogi);
|
Task<User?> ReadUser(ClaimsPrincipal user);
|
||||||
Task<User?> ReadUser(string userName);
|
|
||||||
Task<User?> ReadUser(Guid webSessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GameboardManager : IGameboardManager
|
public class GameboardManager : IGameboardManager
|
||||||
{
|
{
|
||||||
private const int MaxTries = 3;
|
|
||||||
private readonly IGameboardRepository repository;
|
private readonly IGameboardRepository repository;
|
||||||
|
|
||||||
public GameboardManager(IGameboardRepository repository)
|
public GameboardManager(IGameboardRepository repository)
|
||||||
@@ -28,30 +23,21 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> CreateGuestUser(Guid webSessionId)
|
public Task<User?> ReadUser(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
var count = 0;
|
var userId = user.UserId();
|
||||||
while (count < MaxTries)
|
if (user.IsGuest() && Guid.TryParse(userId, out var webSessionId))
|
||||||
{
|
|
||||||
count++;
|
|
||||||
var userName = $"Guest-{Guid.NewGuid()}";
|
|
||||||
var isCreated = await repository.CreateUser(new User(userName, webSessionId));
|
|
||||||
if (isCreated)
|
|
||||||
{
|
|
||||||
return userName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new OperationCanceledException($"Failed to create guest user after {count} tries.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<User?> ReadUser(Guid webSessionId)
|
|
||||||
{
|
{
|
||||||
return repository.ReadGuestUser(webSessionId);
|
return repository.ReadGuestUser(webSessionId);
|
||||||
}
|
}
|
||||||
public Task<User?> ReadUser(string userName)
|
else if (!string.IsNullOrEmpty(userId))
|
||||||
{
|
{
|
||||||
return repository.ReadUser(userName);
|
return repository.ReadUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<User?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> IsPlayer1(string sessionName, string playerName)
|
public async Task<bool> IsPlayer1(string sessionName, string playerName)
|
||||||
{
|
{
|
||||||
//var session = await repository.GetGame(sessionName);
|
//var session = await repository.GetGame(sessionName);
|
||||||
@@ -69,31 +55,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> CreateSession(SessionMetadata session)
|
|
||||||
{
|
|
||||||
return repository.CreateSession(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Session?> ReadSession(string sessionName)
|
|
||||||
{
|
|
||||||
return repository.ReadSession(sessionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves the session to storage.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="session">The session to save.</param>
|
|
||||||
/// <returns>True if the session was saved successfully.</returns>
|
|
||||||
public Task<bool> UpdateSession(SessionMetadata session)
|
|
||||||
{
|
|
||||||
return repository.UpdateSession(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> CreateBoardState(string sessionName, Shogi shogi)
|
|
||||||
{
|
|
||||||
return repository.CreateBoardState(sessionName, shogi);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> AssignPlayer2ToSession(string sessionName, string userName)
|
public async Task<bool> AssignPlayer2ToSession(string sessionName, string userName)
|
||||||
{
|
{
|
||||||
var isSuccess = false;
|
var isSuccess = false;
|
||||||
|
|||||||
@@ -6,20 +6,20 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers
|
namespace Gameboard.ShogiUI.Sockets.Managers
|
||||||
{
|
{
|
||||||
public interface ISocketTokenManager
|
public interface ISocketTokenCache
|
||||||
{
|
{
|
||||||
Guid GenerateToken(string s);
|
Guid GenerateToken(string s);
|
||||||
string GetUsername(Guid g);
|
string? GetUsername(Guid g);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SocketTokenManager : ISocketTokenManager
|
public class SocketTokenCache : ISocketTokenCache
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key is userName or webSessionId
|
/// Key is userName or webSessionId
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<string, Guid> Tokens;
|
private readonly ConcurrentDictionary<string, Guid> Tokens;
|
||||||
|
|
||||||
public SocketTokenManager()
|
public SocketTokenCache()
|
||||||
{
|
{
|
||||||
Tokens = new ConcurrentDictionary<string, Guid>();
|
Tokens = new ConcurrentDictionary<string, Guid>();
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <returns>User name associated to the guid or null.</returns>
|
/// <returns>User name associated to the guid or null.</returns>
|
||||||
public string GetUsername(Guid guid)
|
public string? GetUsername(Guid guid)
|
||||||
{
|
{
|
||||||
var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key;
|
var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key;
|
||||||
if (userName != null)
|
if (userName != null)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
@@ -30,5 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.Models
|
|||||||
{
|
{
|
||||||
Player2 = userName;
|
Player2 = userName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1, Player2 = Player2 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
public string? Player2 { get; private set; }
|
public string? Player2 { get; private set; }
|
||||||
public bool IsPrivate { get; }
|
public bool IsPrivate { get; }
|
||||||
|
|
||||||
public SessionMetadata(string name, bool isPrivate, string player1, string? player2)
|
public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
IsPrivate = isPrivate;
|
IsPrivate = isPrivate;
|
||||||
|
|||||||
@@ -1,18 +1,57 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Models
|
namespace Gameboard.ShogiUI.Sockets.Models
|
||||||
{
|
{
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
public static readonly string GuestPrefix = "Guest-";
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public Guid? WebSessionId { get; }
|
public Guid? WebSessionId { get; }
|
||||||
public bool IsGuest => Name.StartsWith(GuestPrefix) && WebSessionId.HasValue;
|
|
||||||
|
|
||||||
public User(string name, Guid? webSessionId = null)
|
public bool IsGuest => WebSessionId.HasValue;
|
||||||
|
|
||||||
|
public User(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for guest user.
|
||||||
|
/// </summary>
|
||||||
|
public User(string name, Guid webSessionId)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
WebSessionId = webSessionId;
|
WebSessionId = webSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClaimsIdentity CreateMsalUserIdentity()
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>()
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, Name),
|
||||||
|
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||||
|
};
|
||||||
|
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimsIdentity CreateGuestUserIdentity()
|
||||||
|
{
|
||||||
|
// TODO: Make this method static and factory-like.
|
||||||
|
if (!WebSessionId.HasValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot create guest identity without a session identifier.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = new List<Claim>()
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, WebSessionId.Value.ToString()),
|
||||||
|
new Claim(ClaimTypes.Role, "Guest"),
|
||||||
|
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||||
|
};
|
||||||
|
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
Normal file
8
Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.Models
|
||||||
|
{
|
||||||
|
public enum WhichLoginPlatform
|
||||||
|
{
|
||||||
|
Microsoft,
|
||||||
|
Guest
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,11 +18,12 @@
|
|||||||
},
|
},
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchUrl": "Socket/Token",
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "/swagger",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"applicationUrl": "http://127.0.0.1:5100"
|
"applicationUrl": "http://localhost:5100"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
using System;
|
using Gameboard.ShogiUI.Sockets.Models;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
{
|
{
|
||||||
public class UserDocument : CouchDocument
|
public class UserDocument : CouchDocument
|
||||||
{
|
{
|
||||||
public enum LoginPlatform
|
|
||||||
{
|
|
||||||
Microsoft,
|
|
||||||
Guest
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public LoginPlatform Platform { get; set; }
|
public WhichLoginPlatform Platform { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The browser session ID saved via Set-Cookie headers.
|
/// The browser session ID saved via Set-Cookie headers.
|
||||||
/// Only used with guest accounts.
|
/// Only used with guest accounts.
|
||||||
@@ -31,8 +27,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
Name = name;
|
Name = name;
|
||||||
WebSessionId = webSessionId;
|
WebSessionId = webSessionId;
|
||||||
Platform = WebSessionId.HasValue
|
Platform = WebSessionId.HasValue
|
||||||
? LoginPlatform.Guest
|
? WhichLoginPlatform.Guest
|
||||||
: LoginPlatform.Microsoft;
|
: WhichLoginPlatform.Microsoft;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories
|
namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||||
{
|
{
|
||||||
public interface IGameboardRepository
|
public interface IGameboardRepository
|
||||||
{
|
{
|
||||||
Task<bool> CreateBoardState(string sessionName, Models.Shogi shogi);
|
Task<bool> CreateBoardState(Models.Session session);
|
||||||
Task<bool> CreateSession(Models.SessionMetadata session);
|
Task<bool> CreateSession(Models.SessionMetadata session);
|
||||||
Task<bool> CreateUser(Models.User user);
|
Task<bool> CreateUser(Models.User user);
|
||||||
Task<IList<Models.SessionMetadata>> ReadSessionMetadatas();
|
Task<IList<Models.SessionMetadata>> ReadSessionMetadatas();
|
||||||
@@ -46,13 +47,18 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson);
|
var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson);
|
||||||
var response = await client.PostAsync("_find", content);
|
var response = await client.PostAsync("_find", content);
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
var sessions = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent).docs;
|
var results = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent);
|
||||||
|
if (results != null)
|
||||||
return sessions
|
{
|
||||||
|
return results
|
||||||
|
.docs
|
||||||
.Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
|
.Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new List<Models.SessionMetadata>(0);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Models.Session?> ReadSession(string name)
|
public async Task<Models.Session?> ReadSession(string name)
|
||||||
{
|
{
|
||||||
var readShogiTask = ReadShogi(name);
|
var readShogiTask = ReadShogi(name);
|
||||||
@@ -113,9 +119,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
return new Models.Shogi(moves);
|
return new Models.Shogi(moves);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CreateBoardState(string sessionName, Models.Shogi shogi)
|
/// <summary>
|
||||||
|
/// Saves a snapshot of board state and the most recent move.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> CreateBoardState(Models.Session session)
|
||||||
{
|
{
|
||||||
var boardStateDocument = new BoardStateDocument(sessionName, shogi);
|
var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi);
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
||||||
var response = await client.PostAsync(string.Empty, content);
|
var response = await client.PostAsync(string.Empty, content);
|
||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
@@ -197,9 +206,13 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.User?> ReadUser(string userName)
|
public async Task<Models.User?> ReadUser(string userName)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var document = new UserDocument(userName);
|
var document = new UserDocument(userName);
|
||||||
var response = await client.GetAsync(document.Id);
|
var uri = new Uri(client.BaseAddress!, HttpUtility.UrlEncode(document.Id));
|
||||||
|
var response = await client.GetAsync(HttpUtility.UrlEncode(document.Id));
|
||||||
|
var response2 = await client.GetAsync(uri);
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -207,6 +220,11 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
|
|
||||||
return new Models.User(user.Name);
|
return new Models.User(user.Name);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
|
||||||
{
|
|
||||||
public class CreateGameRequestValidator : AbstractValidator<CreateGameRequest>
|
|
||||||
{
|
|
||||||
public CreateGameRequestValidator()
|
|
||||||
{
|
|
||||||
RuleFor(_ => _.Action).Equal(ClientAction.CreateGame);
|
|
||||||
RuleFor(_ => _.GameName).NotEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
|
||||||
{
|
|
||||||
public class ListGamesRequestValidator : AbstractValidator<ListGamesRequest>
|
|
||||||
{
|
|
||||||
public ListGamesRequestValidator()
|
|
||||||
{
|
|
||||||
RuleFor(_ => _.Action).Equal(ClientAction.ListGames);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
|
||||||
{
|
|
||||||
public class LoadGameRequestValidator : AbstractValidator<LoadGameRequest>
|
|
||||||
{
|
|
||||||
public LoadGameRequestValidator()
|
|
||||||
{
|
|
||||||
RuleFor(_ => _.Action).Equal(ClientAction.LoadGame);
|
|
||||||
RuleFor(_ => _.GameName).NotEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
|
||||||
{
|
|
||||||
public class MoveRequestValidator : AbstractValidator<MoveRequest>
|
|
||||||
{
|
|
||||||
public MoveRequestValidator()
|
|
||||||
{
|
|
||||||
RuleFor(_ => _.Action).Equal(ClientAction.Move);
|
|
||||||
RuleFor(_ => _.GameName).NotEmpty();
|
|
||||||
RuleFor(_ => _.Move.From)
|
|
||||||
.Null()
|
|
||||||
.When(_ => _.Move.PieceFromCaptured.HasValue)
|
|
||||||
.WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties.");
|
|
||||||
RuleFor(_ => _.Move.From)
|
|
||||||
.NotEmpty()
|
|
||||||
.When(_ => !_.Move.PieceFromCaptured.HasValue)
|
|
||||||
.WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,65 +31,44 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
private readonly ILogger<SocketService> logger;
|
private readonly ILogger<SocketService> logger;
|
||||||
private readonly ISocketConnectionManager communicationManager;
|
private readonly ISocketConnectionManager communicationManager;
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
private readonly IGameboardRepository gameboardRepository;
|
||||||
private readonly ISocketTokenManager tokenManager;
|
private readonly IGameboardManager gameboardManager;
|
||||||
private readonly ICreateGameHandler createGameHandler;
|
private readonly ISocketTokenCache tokenManager;
|
||||||
private readonly IJoinByCodeHandler joinByCodeHandler;
|
private readonly IJoinByCodeHandler joinByCodeHandler;
|
||||||
private readonly IJoinGameHandler joinGameHandler;
|
private readonly IJoinGameHandler joinGameHandler;
|
||||||
private readonly IListGamesHandler listGamesHandler;
|
|
||||||
private readonly ILoadGameHandler loadGameHandler;
|
|
||||||
private readonly IMoveHandler moveHandler;
|
|
||||||
private readonly IValidator<CreateGameRequest> createGameRequestValidator;
|
|
||||||
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
|
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
|
||||||
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
|
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
|
||||||
private readonly IValidator<ListGamesRequest> listGamesRequestValidator;
|
|
||||||
private readonly IValidator<LoadGameRequest> loadGameRequestValidator;
|
|
||||||
private readonly IValidator<MoveRequest> moveRequestValidator;
|
|
||||||
|
|
||||||
public SocketService(
|
public SocketService(
|
||||||
ILogger<SocketService> logger,
|
ILogger<SocketService> logger,
|
||||||
ISocketConnectionManager communicationManager,
|
ISocketConnectionManager communicationManager,
|
||||||
IGameboardRepository gameboardRepository,
|
IGameboardRepository gameboardRepository,
|
||||||
ISocketTokenManager tokenManager,
|
IGameboardManager gameboardManager,
|
||||||
ICreateGameHandler createGameHandler,
|
ISocketTokenCache tokenManager,
|
||||||
IJoinByCodeHandler joinByCodeHandler,
|
IJoinByCodeHandler joinByCodeHandler,
|
||||||
IJoinGameHandler joinGameHandler,
|
IJoinGameHandler joinGameHandler,
|
||||||
IListGamesHandler listGamesHandler,
|
|
||||||
ILoadGameHandler loadGameHandler,
|
|
||||||
IMoveHandler moveHandler,
|
|
||||||
IValidator<CreateGameRequest> createGameRequestValidator,
|
|
||||||
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
|
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
|
||||||
IValidator<JoinGameRequest> joinGameRequestValidator,
|
IValidator<JoinGameRequest> joinGameRequestValidator
|
||||||
IValidator<ListGamesRequest> listGamesRequestValidator,
|
|
||||||
IValidator<LoadGameRequest> loadGameRequestValidator,
|
|
||||||
IValidator<MoveRequest> moveRequestValidator
|
|
||||||
) : base()
|
) : base()
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.communicationManager = communicationManager;
|
this.communicationManager = communicationManager;
|
||||||
this.gameboardRepository = gameboardRepository;
|
this.gameboardRepository = gameboardRepository;
|
||||||
|
this.gameboardManager = gameboardManager;
|
||||||
this.tokenManager = tokenManager;
|
this.tokenManager = tokenManager;
|
||||||
this.createGameHandler = createGameHandler;
|
|
||||||
this.joinByCodeHandler = joinByCodeHandler;
|
this.joinByCodeHandler = joinByCodeHandler;
|
||||||
this.joinGameHandler = joinGameHandler;
|
this.joinGameHandler = joinGameHandler;
|
||||||
this.listGamesHandler = listGamesHandler;
|
|
||||||
this.loadGameHandler = loadGameHandler;
|
|
||||||
this.moveHandler = moveHandler;
|
|
||||||
this.createGameRequestValidator = createGameRequestValidator;
|
|
||||||
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
|
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
|
||||||
this.joinGameRequestValidator = joinGameRequestValidator;
|
this.joinGameRequestValidator = joinGameRequestValidator;
|
||||||
this.listGamesRequestValidator = listGamesRequestValidator;
|
|
||||||
this.loadGameRequestValidator = loadGameRequestValidator;
|
|
||||||
this.moveRequestValidator = moveRequestValidator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleSocketRequest(HttpContext context)
|
public async Task HandleSocketRequest(HttpContext context)
|
||||||
{
|
{
|
||||||
string? userName = null;
|
string? userName = null;
|
||||||
if (context.Request.Cookies.ContainsKey(SocketController.WebSessionKey))
|
var user = await gameboardManager.ReadUser(context.User);
|
||||||
|
if (user?.WebSessionId != null)
|
||||||
{
|
{
|
||||||
// Guest account
|
// Guest account
|
||||||
var webSessionId = Guid.Parse(context.Request.Cookies[SocketController.WebSessionKey]!);
|
userName = tokenManager.GetUsername(user.WebSessionId.Value);
|
||||||
userName = (await gameboardRepository.ReadGuestUser(webSessionId))?.Name;
|
|
||||||
}
|
}
|
||||||
else if (context.Request.Query.Keys.Contains("token"))
|
else if (context.Request.Query.Keys.Contains("token"))
|
||||||
{
|
{
|
||||||
@@ -123,24 +102,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
}
|
}
|
||||||
switch (request.Action)
|
switch (request.Action)
|
||||||
{
|
{
|
||||||
case ClientAction.ListGames:
|
|
||||||
{
|
|
||||||
var req = JsonConvert.DeserializeObject<ListGamesRequest>(message);
|
|
||||||
if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req))
|
|
||||||
{
|
|
||||||
await listGamesHandler.Handle(req, userName);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ClientAction.CreateGame:
|
|
||||||
{
|
|
||||||
var req = JsonConvert.DeserializeObject<CreateGameRequest>(message);
|
|
||||||
if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req))
|
|
||||||
{
|
|
||||||
await createGameHandler.Handle(req, userName);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ClientAction.JoinGame:
|
case ClientAction.JoinGame:
|
||||||
{
|
{
|
||||||
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
|
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
|
||||||
@@ -159,24 +120,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ClientAction.LoadGame:
|
|
||||||
{
|
|
||||||
var req = JsonConvert.DeserializeObject<LoadGameRequest>(message);
|
|
||||||
if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req))
|
|
||||||
{
|
|
||||||
await loadGameHandler.Handle(req, userName);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ClientAction.Move:
|
|
||||||
{
|
|
||||||
var req = JsonConvert.DeserializeObject<MoveRequest>(message);
|
|
||||||
if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req))
|
|
||||||
{
|
|
||||||
await moveHandler.Handle(req, userName);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
|
|||||||
46
Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
Normal file
46
Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Standardizes the claims from third party issuers. Also registers new msal users in the database.
|
||||||
|
/// </summary>
|
||||||
|
public class ShogiUserClaimsTransformer : IClaimsTransformation
|
||||||
|
{
|
||||||
|
private static readonly string MsalUsernameClaim = "preferred_username";
|
||||||
|
private readonly IGameboardRepository gameboardRepository;
|
||||||
|
|
||||||
|
public ShogiUserClaimsTransformer(IGameboardRepository gameboardRepository)
|
||||||
|
{
|
||||||
|
this.gameboardRepository = gameboardRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||||
|
{
|
||||||
|
var nameClaim = principal.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim);
|
||||||
|
if (nameClaim != default)
|
||||||
|
{
|
||||||
|
var user = await gameboardRepository.ReadUser(nameClaim.Value);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
var newUser = new Models.User(nameClaim.Value);
|
||||||
|
var success = await gameboardRepository.CreateUser(newUser);
|
||||||
|
if (success) user = newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
return new ClaimsPrincipal(user.CreateMsalUserIdentity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,17 +6,23 @@ using Gameboard.ShogiUI.Sockets.Repositories;
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||||
using Gameboard.ShogiUI.Sockets.Services;
|
using Gameboard.ShogiUI.Sockets.Services;
|
||||||
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
|
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Identity.Client;
|
||||||
|
using Microsoft.Identity.Web;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -34,29 +40,16 @@ namespace Gameboard.ShogiUI.Sockets
|
|||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Socket ActionHandlers
|
|
||||||
services.AddSingleton<ICreateGameHandler, CreateGameHandler>();
|
|
||||||
services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
|
services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
|
||||||
services.AddSingleton<IJoinGameHandler, JoinGameHandler>();
|
services.AddSingleton<IJoinGameHandler, JoinGameHandler>();
|
||||||
services.AddSingleton<IListGamesHandler, ListGamesHandler>();
|
|
||||||
services.AddSingleton<ILoadGameHandler, LoadGameHandler>();
|
|
||||||
services.AddSingleton<IMoveHandler, MoveHandler>();
|
|
||||||
|
|
||||||
// Managers
|
|
||||||
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
||||||
services.AddSingleton<ISocketTokenManager, SocketTokenManager>();
|
services.AddSingleton<ISocketTokenCache, SocketTokenCache>();
|
||||||
services.AddSingleton<IGameboardManager, GameboardManager>();
|
services.AddSingleton<IGameboardManager, GameboardManager>();
|
||||||
|
|
||||||
// Services
|
|
||||||
services.AddSingleton<IValidator<CreateGameRequest>, CreateGameRequestValidator>();
|
|
||||||
services.AddSingleton<IValidator<JoinByCodeRequest>, JoinByCodeRequestValidator>();
|
services.AddSingleton<IValidator<JoinByCodeRequest>, JoinByCodeRequestValidator>();
|
||||||
services.AddSingleton<IValidator<JoinGameRequest>, JoinGameRequestValidator>();
|
services.AddSingleton<IValidator<JoinGameRequest>, JoinGameRequestValidator>();
|
||||||
services.AddSingleton<IValidator<ListGamesRequest>, ListGamesRequestValidator>();
|
|
||||||
services.AddSingleton<IValidator<LoadGameRequest>, LoadGameRequestValidator>();
|
|
||||||
services.AddSingleton<IValidator<MoveRequest>, MoveRequestValidator>();
|
|
||||||
services.AddSingleton<ISocketService, SocketService>();
|
services.AddSingleton<ISocketService, SocketService>();
|
||||||
|
services.AddTransient<IGameboardRepository, GameboardRepository>();
|
||||||
// Repositories
|
services.AddSingleton<IClaimsTransformation, ShogiUserClaimsTransformer>();
|
||||||
services.AddHttpClient("couchdb", c =>
|
services.AddHttpClient("couchdb", c =>
|
||||||
{
|
{
|
||||||
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin"));
|
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin"));
|
||||||
@@ -66,35 +59,54 @@ namespace Gameboard.ShogiUI.Sockets
|
|||||||
var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/";
|
var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/";
|
||||||
c.BaseAddress = new Uri(baseUrl);
|
c.BaseAddress = new Uri(baseUrl);
|
||||||
});
|
});
|
||||||
services.AddTransient<IGameboardRepository, GameboardRepository>();
|
|
||||||
//services.AddSingleton<IAuthenticatedHttpClient, AuthenticatedHttpClient>();
|
|
||||||
//services.AddSingleton<ICouchClient>(provider => new CouchClient(databaseName, couchUrl));
|
|
||||||
|
|
||||||
services.AddControllers();
|
|
||||||
|
|
||||||
services
|
services
|
||||||
.AddAuthentication(options =>
|
.AddControllers()
|
||||||
|
.AddNewtonsoftJson(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.SerializerSettings.Formatting = Formatting.Indented;
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.SerializerSettings.ContractResolver = new DefaultContractResolver
|
||||||
})
|
|
||||||
.AddJwtBearer(options =>
|
|
||||||
{
|
{
|
||||||
options.Authority = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0";
|
NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true }
|
||||||
options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122";
|
};
|
||||||
options.TokenValidationParameters.ValidateIssuer = true;
|
options.SerializerSettings.Converters = new[] { new StringEnumConverter() };
|
||||||
options.TokenValidationParameters.ValidateAudience = true;
|
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||||
|
});
|
||||||
|
|
||||||
options.Events = new JwtBearerEvents
|
services.AddAuthentication("CookieOrJwt")
|
||||||
|
.AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options =>
|
||||||
{
|
{
|
||||||
OnMessageReceived = (context) =>
|
options.ForwardDefaultSelector = context =>
|
||||||
{
|
{
|
||||||
if (context.HttpContext.WebSockets.IsWebSocketRequest)
|
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
|
||||||
|
return bearerAuth
|
||||||
|
? JwtBearerDefaults.AuthenticationScheme
|
||||||
|
: CookieAuthenticationDefaults.AuthenticationScheme;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.AddCookie(options =>
|
||||||
{
|
{
|
||||||
Console.WriteLine("Yep");
|
options.Cookie.Name = "session-id";
|
||||||
}
|
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
|
||||||
return Task.FromResult(0);
|
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
|
||||||
}
|
options.SlidingExpiration = true;
|
||||||
|
})
|
||||||
|
.AddMicrosoftIdentityWebApi(Configuration);
|
||||||
|
|
||||||
|
services.AddSwaggerDocument(config =>
|
||||||
|
{
|
||||||
|
config.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Type = NSwag.OpenApiSecuritySchemeType.OAuth2,
|
||||||
|
Flow = NSwag.OpenApiOAuth2Flow.AccessCode,
|
||||||
|
AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||||
|
TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||||
|
Scopes = new Dictionary<string, string> { { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" } },
|
||||||
|
Scheme = "Bearer"
|
||||||
|
});
|
||||||
|
config.PostProcess = document =>
|
||||||
|
{
|
||||||
|
document.Info.Title = "Gameboard.ShogiUI.Sockets";
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -114,6 +126,18 @@ namespace Gameboard.ShogiUI.Sockets
|
|||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
var client = PublicClientApplicationBuilder
|
||||||
|
.Create(Configuration["AzureAd:ClientId"])
|
||||||
|
.WithLogging(
|
||||||
|
(level, message, pii) =>
|
||||||
|
{
|
||||||
|
|
||||||
|
},
|
||||||
|
LogLevel.Verbose,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.Build();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -121,17 +145,20 @@ namespace Gameboard.ShogiUI.Sockets
|
|||||||
}
|
}
|
||||||
app
|
app
|
||||||
.UseRequestResponseLogging()
|
.UseRequestResponseLogging()
|
||||||
.UseCors(
|
.UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials())
|
||||||
opt => opt
|
|
||||||
.WithOrigins(origins)
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.WithExposedHeaders("Set-Cookie")
|
|
||||||
.AllowCredentials()
|
|
||||||
)
|
|
||||||
.UseRouting()
|
.UseRouting()
|
||||||
.UseAuthentication()
|
.UseAuthentication()
|
||||||
.UseAuthorization()
|
.UseAuthorization()
|
||||||
|
.UseOpenApi()
|
||||||
|
.UseSwaggerUi3(config =>
|
||||||
|
{
|
||||||
|
config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings()
|
||||||
|
{
|
||||||
|
ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff",
|
||||||
|
UsePkceWithAuthorizationCodeGrant = true
|
||||||
|
};
|
||||||
|
//config.WithCredentials = true;
|
||||||
|
})
|
||||||
.UseWebSockets(socketOptions)
|
.UseWebSockets(socketOptions)
|
||||||
.UseEndpoints(endpoints =>
|
.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace Gameboard.ShogiUI.Sockets.Utilities
|
|||||||
{
|
{
|
||||||
var file = (char)(x + A);
|
var file = (char)(x + A);
|
||||||
var rank = y + 1;
|
var rank = y + 1;
|
||||||
Console.WriteLine($"({x},{y}) - {file}{rank}");
|
|
||||||
return $"{file}{rank}";
|
return $"{file}{rank}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,5 +12,11 @@
|
|||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"AzureAd": {
|
||||||
|
"Instance": "https://login.microsoftonline.com/",
|
||||||
|
"ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff",
|
||||||
|
"TenantId": "common",
|
||||||
|
"Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff"
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
|
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
|
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.0.2">
|
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
using AutoFixture;
|
|
||||||
using FluentAssertions;
|
|
||||||
using FluentAssertions.Execution;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.xUnitTests.RequestValidators
|
|
||||||
{
|
|
||||||
public class MoveRequestValidatorShould
|
|
||||||
{
|
|
||||||
private readonly Fixture fixture;
|
|
||||||
private readonly MoveRequestValidator validator;
|
|
||||||
public MoveRequestValidatorShould()
|
|
||||||
{
|
|
||||||
fixture = new Fixture();
|
|
||||||
validator = new MoveRequestValidator();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidPropertyCombinations()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var request = fixture.Create<MoveRequest>();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var results = validator.Validate(request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
results.IsValid.Should().BeFalse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AllowValidPropertyCombinations()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var requestWithoutFrom = new MoveRequest()
|
|
||||||
{
|
|
||||||
Action = ClientAction.Move,
|
|
||||||
GameName = "Some game name",
|
|
||||||
Move = new Move()
|
|
||||||
{
|
|
||||||
IsPromotion = false,
|
|
||||||
PieceFromCaptured = WhichPiece.Bishop,
|
|
||||||
To = "A4"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var requestWithoutPieceFromCaptured = new MoveRequest()
|
|
||||||
{
|
|
||||||
Action = ClientAction.Move,
|
|
||||||
GameName = "Some game name",
|
|
||||||
Move = new Move()
|
|
||||||
{
|
|
||||||
From = "A1",
|
|
||||||
IsPromotion = false,
|
|
||||||
To = "A4"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var results = validator.Validate(requestWithoutFrom);
|
|
||||||
var results2 = validator.Validate(requestWithoutPieceFromCaptured);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
results.IsValid.Should().BeTrue();
|
|
||||||
results2.IsValid.Should().BeTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -72,7 +72,6 @@ namespace PathFinding
|
|||||||
var element = collection[from];
|
var element = collection[from];
|
||||||
if (element == null)
|
if (element == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Null element in PathEvery");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
foreach (var path in element.MoveSet.GetMoves())
|
foreach (var path in element.MoveSet.GetMoves())
|
||||||
|
|||||||
Reference in New Issue
Block a user