diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj
index 4e77937..98fe81e 100644
--- a/Benchmarking/Benchmarking.csproj
+++ b/Benchmarking/Benchmarking.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs
new file mode 100644
index 0000000..c15dae4
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs
@@ -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 MoveHistory { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
index 153b1d9..012c6ea 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
@@ -5,9 +5,6 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{
public class PostMove
{
- [Required]
- public string GameName { get; set; }
-
[Required]
public Move Move { get; set; }
}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs
index de71f67..ebe9665 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs
@@ -3,8 +3,6 @@
public class PostSession
{
public string Name { get; set; }
- public string Player1 { get; set; }
- public string Player2 { get; set; }
public bool IsPrivate { get; set; }
}
}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs
index 6aa082d..e1b59bb 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs
@@ -1,27 +1,21 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System;
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 string Action { get; }
- public string Error { get; set; }
public Game Game { get; set; }
+
+ ///
+ /// The player who created the game.
+ ///
public string PlayerName { get; set; }
public CreateGameResponse()
{
Action = ClientAction.CreateGame.ToString();
- Error = string.Empty;
- Game = new Game();
- PlayerName = string.Empty;
}
}
}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs
index 77f0dc2..69d18c6 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs
@@ -3,6 +3,5 @@
public interface IResponse
{
string Action { get; }
- string Error { get; set; }
}
}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs
deleted file mode 100644
index d8e5556..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs
+++ /dev/null
@@ -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 Games { get; set; }
-
- public ListGamesResponse()
- {
- Action = ClientAction.ListGames.ToString();
- Error = "";
- Games = new Collection();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs
deleted file mode 100644
index 09884cb..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs
+++ /dev/null
@@ -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 MoveHistory { get; set; }
- public string Error { get; set; }
-
- public LoadGameResponse()
- {
- Action = ClientAction.LoadGame.ToString();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs
index d7b9e91..0853998 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs
@@ -1,29 +1,19 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System.Collections.Generic;
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 string Action { get; }
- public string Error { get; set; }
- public string GameName { get; set; }
- public string PlayerName { get; set; }
- public Move Move { get; set; }
+ public string Action { get; protected set; }
+ public Game Game { get; set; }
+ public WhichPlayer PlayerPerspective { get; set; }
+ public BoardState BoardState { get; set; }
+ public IList MoveHistory { get; set; }
public MoveResponse()
{
Action = ClientAction.Move.ToString();
- Error = string.Empty;
- GameName = string.Empty;
- PlayerName = string.Empty;
- Move = new Move();
}
}
}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs
index f3a655b..99b9446 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs
@@ -2,11 +2,9 @@
{
public enum ClientAction
{
- ListGames,
CreateGame,
JoinGame,
JoinByCode,
- LoadGame,
Move
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
index 57fb268..718573f 100644
--- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
+++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
@@ -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.ServiceModels.Api;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
@@ -10,16 +12,15 @@ using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Controllers
{
- [Authorize]
+
[ApiController]
[Route("[controller]")]
+ [Authorize(Roles = "Shogi")]
public class GameController : ControllerBase
{
- private static readonly string UsernameClaim = "preferred_username";
private readonly IGameboardManager gameboardManager;
private readonly IGameboardRepository gameboardRepository;
private readonly ISocketConnectionManager communicationManager;
- private string? JwtUserName => HttpContext.User.Claims.FirstOrDefault(c => c.Type == UsernameClaim)?.Value;
public GameController(
IGameboardRepository repository,
@@ -68,22 +69,12 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
[HttpPost("{gameName}/Move")]
public async Task PostMove([FromRoute] string gameName, [FromBody] PostMove request)
{
- Models.User? user = null;
- if (Request.Cookies.ContainsKey(SocketController.WebSessionKey))
- {
- 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);
+ var user = await gameboardManager.ReadUser(User);
+ var session = await gameboardRepository.ReadSession(gameName);
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;
@@ -94,13 +85,19 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
if (moveSuccess)
{
+ var createSuccess = await gameboardRepository.CreateBoardState(session);
+ if (!createSuccess)
+ {
+ throw new ApplicationException("Unable to persist board state.");
+ }
await communicationManager.BroadcastToPlayers(new MoveResponse
{
- GameName = session.Name,
- PlayerName = user.Name,
- Move = moveModel.ToServiceModel()
+ BoardState = session.Shogi.ToServiceModel(),
+ Game = session.ToServiceModel(),
+ MoveHistory = session.Shogi.MoveHistory.Select(h => h.ToServiceModel()).ToList(),
+ PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2
}, session.Player1, session.Player2);
- return Created(string.Empty, null);
+ return Ok();
}
throw new InvalidOperationException("Illegal move.");
}
@@ -124,5 +121,56 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
// }
// return new ConflictResult();
//}
+
+ [HttpPost]
+ public async Task 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();
+
+ }
+
+ ///
+ /// Reads the board session and subscribes the caller to socket events for that session.
+ ///
+ [HttpGet("{gameName}")]
+ public async Task 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 GetSessions()
+ {
+ var sessions = await gameboardRepository.ReadSessionMetadatas();
+ return new JsonResult(sessions.Select(s => s.ToServiceModel()).ToList());
+ }
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
index e5ee6db..0b9e941 100644
--- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
+++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
@@ -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.ServiceModels.Api;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
-using System.Linq;
+using System.Security.Claims;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Controllers
{
- [Authorize]
- [Route("[controller]")]
[ApiController]
+ [Route("[controller]")]
+ [Authorize(Roles = "Shogi")]
public class SocketController : ControllerBase
{
- public static readonly string WebSessionKey = "session-id";
private readonly ILogger logger;
- private readonly ISocketTokenManager tokenManager;
+ private readonly ISocketTokenCache tokenCache;
private readonly IGameboardManager gameboardManager;
private readonly IGameboardRepository gameboardRepository;
- private readonly CookieOptions createSessionOptions;
- private readonly CookieOptions deleteSessionOptions;
+ private readonly AuthenticationProperties authenticationProps;
public SocketController(
ILogger logger,
- ISocketTokenManager tokenManager,
+ ISocketTokenCache tokenCache,
IGameboardManager gameboardManager,
IGameboardRepository gameboardRepository)
{
this.logger = logger;
- this.tokenManager = tokenManager;
+ this.tokenCache = tokenCache;
this.gameboardManager = gameboardManager;
this.gameboardRepository = gameboardRepository;
- createSessionOptions = new CookieOptions
+ authenticationProps = new AuthenticationProperties
{
- Secure = true,
- HttpOnly = true,
- SameSite = SameSiteMode.None,
- Expires = DateTimeOffset.Now.AddYears(5)
+ AllowRefresh = true,
+ IsPersistent = true
};
- deleteSessionOptions = new CookieOptions();
}
- [HttpGet("Yep")]
+ [HttpGet("GuestLogout")]
[AllowAnonymous]
- public IActionResult Yep()
+ public async Task GuestLogout()
{
- deleteSessionOptions.Expires = DateTimeOffset.Now.AddDays(-1);
- Response.Cookies.Append(WebSessionKey, "", deleteSessionOptions);
+ await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
[HttpGet("Token")]
- public IActionResult GetToken()
+ public async Task GetToken()
{
- var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
- var token = tokenManager.GenerateToken(userName);
+ var identityId = User.UserId();
+ 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));
}
- ///
- /// 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.
- ///
- ///
- ///
- [AllowAnonymous]
[HttpGet("GuestToken")]
+ [AllowAnonymous]
public async Task GetGuestToken()
{
- var cookies = Request.Cookies;
- var webSessionId = cookies.ContainsKey(WebSessionKey)
- ? Guid.Parse(cookies[WebSessionKey]!)
- : Guid.NewGuid();
- var webSessionIdAsString = webSessionId.ToString();
-
- var user = await gameboardRepository.ReadGuestUser(webSessionId);
- if (user == null)
+ if (Guid.TryParse(User.UserId(), out Guid webSessionId))
{
- var userName = await gameboardManager.CreateGuestUser(webSessionId);
- var token = tokenManager.GenerateToken(webSessionIdAsString);
- Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
- return new JsonResult(new GetGuestTokenResponse(userName, token));
+ var user = await gameboardRepository.ReadGuestUser(webSessionId);
+ if (user != null)
+ {
+ var token = tokenCache.GenerateToken(webSessionId.ToString());
+ return new JsonResult(new GetGuestTokenResponse(user.Name, token));
+ }
}
else
{
- var token = tokenManager.GenerateToken(webSessionIdAsString);
- Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
- return new JsonResult(new GetGuestTokenResponse(user.Name, token));
+ // 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();
}
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
new file mode 100644
index 0000000..30517ad
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
@@ -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");
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
index 241f8e8..bd64225 100644
--- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
+++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
@@ -8,13 +8,15 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs
deleted file mode 100644
index 2041ef8..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs
deleted file mode 100644
index b92c8ca..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs
deleted file mode 100644
index d82c880..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs
+++ /dev/null
@@ -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);
- }
-
- ///
- /// Subscribes a user to messages for a session and loads that session into the BoardManager for playing.
- ///
- public class LoadGameHandler : ILoadGameHandler
- {
- private readonly ILogger logger;
- private readonly IGameboardRepository gameboardRepository;
- private readonly ISocketConnectionManager communicationManager;
-
- public LoadGameHandler(
- ILogger 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);
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs
deleted file mode 100644
index 706e125..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs
+++ /dev/null
@@ -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);
- }
- }
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs
index 365722f..e4c4aa0 100644
--- a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs
+++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs
@@ -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 System;
+using System.Security.Claims;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers
{
public interface IGameboardManager
{
- Task CreateGuestUser(Guid webSessionId);
Task IsPlayer1(string sessionName, string playerName);
- Task CreateSession(SessionMetadata session);
- Task ReadSession(string gameName);
- Task UpdateSession(SessionMetadata session);
Task AssignPlayer2ToSession(string sessionName, string userName);
- Task CreateBoardState(string sessionName, Shogi shogi);
- Task ReadUser(string userName);
- Task ReadUser(Guid webSessionId);
+ Task ReadUser(ClaimsPrincipal user);
}
public class GameboardManager : IGameboardManager
{
- private const int MaxTries = 3;
private readonly IGameboardRepository repository;
public GameboardManager(IGameboardRepository repository)
@@ -28,30 +23,21 @@ namespace Gameboard.ShogiUI.Sockets.Managers
this.repository = repository;
}
- public async Task CreateGuestUser(Guid webSessionId)
+ public Task ReadUser(ClaimsPrincipal user)
{
- var count = 0;
- while (count < MaxTries)
+ var userId = user.UserId();
+ 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;
- }
+ return repository.ReadGuestUser(webSessionId);
}
- throw new OperationCanceledException($"Failed to create guest user after {count} tries.");
+ else if (!string.IsNullOrEmpty(userId))
+ {
+ return repository.ReadUser(userId);
+ }
+
+ return Task.FromResult(null);
}
- public Task ReadUser(Guid webSessionId)
- {
- return repository.ReadGuestUser(webSessionId);
- }
- public Task ReadUser(string userName)
- {
- return repository.ReadUser(userName);
- }
public async Task IsPlayer1(string sessionName, string playerName)
{
//var session = await repository.GetGame(sessionName);
@@ -69,31 +55,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers
return string.Empty;
}
- public Task CreateSession(SessionMetadata session)
- {
- return repository.CreateSession(session);
- }
-
- public Task ReadSession(string sessionName)
- {
- return repository.ReadSession(sessionName);
- }
-
- ///
- /// Saves the session to storage.
- ///
- /// The session to save.
- /// True if the session was saved successfully.
- public Task UpdateSession(SessionMetadata session)
- {
- return repository.UpdateSession(session);
- }
-
- public Task CreateBoardState(string sessionName, Shogi shogi)
- {
- return repository.CreateBoardState(sessionName, shogi);
- }
-
public async Task AssignPlayer2ToSession(string sessionName, string userName)
{
var isSuccess = false;
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs
similarity index 83%
rename from Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs
rename to Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs
index d8c46ab..722dc33 100644
--- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs
+++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs
@@ -6,20 +6,20 @@ using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers
{
- public interface ISocketTokenManager
+ public interface ISocketTokenCache
{
Guid GenerateToken(string s);
- string GetUsername(Guid g);
+ string? GetUsername(Guid g);
}
- public class SocketTokenManager : ISocketTokenManager
+ public class SocketTokenCache : ISocketTokenCache
{
///
/// Key is userName or webSessionId
///
private readonly ConcurrentDictionary Tokens;
- public SocketTokenManager()
+ public SocketTokenCache()
{
Tokens = new ConcurrentDictionary();
}
@@ -41,7 +41,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
}
/// User name associated to the guid or null.
- public string GetUsername(Guid guid)
+ public string? GetUsername(Guid guid)
{
var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key;
if (userName != null)
diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs
index 32bd545..67240e0 100644
--- a/Gameboard.ShogiUI.Sockets/Models/Session.cs
+++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs
@@ -1,4 +1,5 @@
-using Newtonsoft.Json;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using Newtonsoft.Json;
using System.Collections.Concurrent;
using System.Net.WebSockets;
@@ -30,5 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.Models
{
Player2 = userName;
}
+
+ public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1, Player2 = Player2 };
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs
index f3cfade..d46b8af 100644
--- a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs
+++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs
@@ -10,7 +10,7 @@
public string? Player2 { get; private set; }
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;
IsPrivate = isPrivate;
diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs
index ba6959d..acfd4a9 100644
--- a/Gameboard.ShogiUI.Sockets/Models/User.cs
+++ b/Gameboard.ShogiUI.Sockets/Models/User.cs
@@ -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
{
public class User
{
- public static readonly string GuestPrefix = "Guest-";
public string Name { 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;
+ }
+
+ ///
+ /// Constructor for guest user.
+ ///
+ public User(string name, Guid webSessionId)
{
Name = name;
WebSessionId = webSessionId;
}
+
+ public ClaimsIdentity CreateMsalUserIdentity()
+ {
+ var claims = new List()
+ {
+ 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()
+ {
+ 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);
+ }
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
new file mode 100644
index 0000000..5923404
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
@@ -0,0 +1,8 @@
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ public enum WhichLoginPlatform
+ {
+ Microsoft,
+ Guest
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
index 4a087fc..230be61 100644
--- a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
+++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
@@ -18,11 +18,12 @@
},
"Kestrel": {
"commandName": "Project",
- "launchUrl": "Socket/Token",
+ "launchBrowser": true,
+ "launchUrl": "/swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
- "applicationUrl": "http://127.0.0.1:5100"
+ "applicationUrl": "http://localhost:5100"
}
}
}
\ No newline at end of file
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs
index 843094e..c41b60b 100644
--- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs
@@ -1,17 +1,13 @@
-using System;
+using Gameboard.ShogiUI.Sockets.Models;
+using System;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class UserDocument : CouchDocument
{
- public enum LoginPlatform
- {
- Microsoft,
- Guest
- }
public string Name { get; set; }
- public LoginPlatform Platform { get; set; }
+ public WhichLoginPlatform Platform { get; set; }
///
/// The browser session ID saved via Set-Cookie headers.
/// Only used with guest accounts.
@@ -31,8 +27,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
Name = name;
WebSessionId = webSessionId;
Platform = WebSessionId.HasValue
- ? LoginPlatform.Guest
- : LoginPlatform.Microsoft;
+ ? WhichLoginPlatform.Guest
+ : WhichLoginPlatform.Microsoft;
}
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
index fce2aaa..89ee28c 100644
--- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
+++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
@@ -7,12 +7,13 @@ using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
+using System.Web;
namespace Gameboard.ShogiUI.Sockets.Repositories
{
public interface IGameboardRepository
{
- Task CreateBoardState(string sessionName, Models.Shogi shogi);
+ Task CreateBoardState(Models.Session session);
Task CreateSession(Models.SessionMetadata session);
Task CreateUser(Models.User user);
Task> ReadSessionMetadatas();
@@ -46,11 +47,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync();
- var sessions = JsonConvert.DeserializeObject>(responseContent).docs;
+ var results = JsonConvert.DeserializeObject>(responseContent);
+ if (results != null)
+ {
+ return results
+ .docs
+ .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
+ .ToList();
+ }
- return sessions
- .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
- .ToList();
+ return new List(0);
}
public async Task ReadSession(string name)
@@ -113,9 +119,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return new Models.Shogi(moves);
}
- public async Task CreateBoardState(string sessionName, Models.Shogi shogi)
+ ///
+ /// Saves a snapshot of board state and the most recent move.
+ ///
+ public async Task 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 response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
@@ -198,14 +207,23 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
public async Task ReadUser(string userName)
{
- var document = new UserDocument(userName);
- var response = await client.GetAsync(document.Id);
- var responseContent = await response.Content.ReadAsStringAsync();
- if (response.IsSuccessStatusCode)
+ try
{
- var user = JsonConvert.DeserializeObject(responseContent);
+ var document = new UserDocument(userName);
+ 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();
+ if (response.IsSuccessStatusCode)
+ {
+ var user = JsonConvert.DeserializeObject(responseContent);
- return new Models.User(user.Name);
+ return new Models.User(user.Name);
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
}
return null;
}
diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs
deleted file mode 100644
index 033b218..0000000
--- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs
+++ /dev/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
- {
- public CreateGameRequestValidator()
- {
- RuleFor(_ => _.Action).Equal(ClientAction.CreateGame);
- RuleFor(_ => _.GameName).NotEmpty();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs
deleted file mode 100644
index 38ecc66..0000000
--- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs
+++ /dev/null
@@ -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
- {
- public ListGamesRequestValidator()
- {
- RuleFor(_ => _.Action).Equal(ClientAction.ListGames);
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs
deleted file mode 100644
index 3fb477d..0000000
--- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs
+++ /dev/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 LoadGameRequestValidator : AbstractValidator
- {
- public LoadGameRequestValidator()
- {
- RuleFor(_ => _.Action).Equal(ClientAction.LoadGame);
- RuleFor(_ => _.GameName).NotEmpty();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs
deleted file mode 100644
index 29a0cb9..0000000
--- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs
+++ /dev/null
@@ -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
- {
- 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.");
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs
index 925c018..30fff7f 100644
--- a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs
+++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs
@@ -31,65 +31,44 @@ namespace Gameboard.ShogiUI.Sockets.Services
private readonly ILogger logger;
private readonly ISocketConnectionManager communicationManager;
private readonly IGameboardRepository gameboardRepository;
- private readonly ISocketTokenManager tokenManager;
- private readonly ICreateGameHandler createGameHandler;
+ private readonly IGameboardManager gameboardManager;
+ private readonly ISocketTokenCache tokenManager;
private readonly IJoinByCodeHandler joinByCodeHandler;
private readonly IJoinGameHandler joinGameHandler;
- private readonly IListGamesHandler listGamesHandler;
- private readonly ILoadGameHandler loadGameHandler;
- private readonly IMoveHandler moveHandler;
- private readonly IValidator createGameRequestValidator;
private readonly IValidator joinByCodeRequestValidator;
private readonly IValidator joinGameRequestValidator;
- private readonly IValidator listGamesRequestValidator;
- private readonly IValidator loadGameRequestValidator;
- private readonly IValidator moveRequestValidator;
public SocketService(
ILogger logger,
ISocketConnectionManager communicationManager,
IGameboardRepository gameboardRepository,
- ISocketTokenManager tokenManager,
- ICreateGameHandler createGameHandler,
+ IGameboardManager gameboardManager,
+ ISocketTokenCache tokenManager,
IJoinByCodeHandler joinByCodeHandler,
IJoinGameHandler joinGameHandler,
- IListGamesHandler listGamesHandler,
- ILoadGameHandler loadGameHandler,
- IMoveHandler moveHandler,
- IValidator createGameRequestValidator,
IValidator joinByCodeRequestValidator,
- IValidator joinGameRequestValidator,
- IValidator listGamesRequestValidator,
- IValidator loadGameRequestValidator,
- IValidator moveRequestValidator
+ IValidator joinGameRequestValidator
) : base()
{
this.logger = logger;
this.communicationManager = communicationManager;
this.gameboardRepository = gameboardRepository;
+ this.gameboardManager = gameboardManager;
this.tokenManager = tokenManager;
- this.createGameHandler = createGameHandler;
this.joinByCodeHandler = joinByCodeHandler;
this.joinGameHandler = joinGameHandler;
- this.listGamesHandler = listGamesHandler;
- this.loadGameHandler = loadGameHandler;
- this.moveHandler = moveHandler;
- this.createGameRequestValidator = createGameRequestValidator;
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
this.joinGameRequestValidator = joinGameRequestValidator;
- this.listGamesRequestValidator = listGamesRequestValidator;
- this.loadGameRequestValidator = loadGameRequestValidator;
- this.moveRequestValidator = moveRequestValidator;
}
public async Task HandleSocketRequest(HttpContext context)
{
string? userName = null;
- if (context.Request.Cookies.ContainsKey(SocketController.WebSessionKey))
+ var user = await gameboardManager.ReadUser(context.User);
+ if (user?.WebSessionId != null)
{
// Guest account
- var webSessionId = Guid.Parse(context.Request.Cookies[SocketController.WebSessionKey]!);
- userName = (await gameboardRepository.ReadGuestUser(webSessionId))?.Name;
+ userName = tokenManager.GetUsername(user.WebSessionId.Value);
}
else if (context.Request.Query.Keys.Contains("token"))
{
@@ -123,24 +102,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
}
switch (request.Action)
{
- case ClientAction.ListGames:
- {
- var req = JsonConvert.DeserializeObject(message);
- if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req))
- {
- await listGamesHandler.Handle(req, userName);
- }
- break;
- }
- case ClientAction.CreateGame:
- {
- var req = JsonConvert.DeserializeObject(message);
- if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req))
- {
- await createGameHandler.Handle(req, userName);
- }
- break;
- }
case ClientAction.JoinGame:
{
var req = JsonConvert.DeserializeObject(message);
@@ -159,24 +120,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
}
break;
}
- case ClientAction.LoadGame:
- {
- var req = JsonConvert.DeserializeObject(message);
- if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req))
- {
- await loadGameHandler.Handle(req, userName);
- }
- break;
- }
- case ClientAction.Move:
- {
- var req = JsonConvert.DeserializeObject(message);
- if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req))
- {
- await moveHandler.Handle(req, userName);
- }
- break;
- }
}
}
catch (OperationCanceledException ex)
diff --git a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
new file mode 100644
index 0000000..1cbc725
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
@@ -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
+{
+ ///
+ /// Standardizes the claims from third party issuers. Also registers new msal users in the database.
+ ///
+ 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 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;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs
index 32cd211..92f88e5 100644
--- a/Gameboard.ShogiUI.Sockets/Startup.cs
+++ b/Gameboard.ShogiUI.Sockets/Startup.cs
@@ -6,17 +6,23 @@ using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.Services;
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Web;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Security.Claims;
using System.Text;
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.
public void ConfigureServices(IServiceCollection services)
{
- // Socket ActionHandlers
- services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- // Managers
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
-
- // Services
- services.AddSingleton, CreateGameRequestValidator>();
services.AddSingleton, JoinByCodeRequestValidator>();
services.AddSingleton, JoinGameRequestValidator>();
- services.AddSingleton, ListGamesRequestValidator>();
- services.AddSingleton, LoadGameRequestValidator>();
- services.AddSingleton, MoveRequestValidator>();
services.AddSingleton();
-
- // Repositories
+ services.AddTransient();
+ services.AddSingleton();
services.AddHttpClient("couchdb", c =>
{
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin"));
@@ -66,37 +59,56 @@ namespace Gameboard.ShogiUI.Sockets
var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/";
c.BaseAddress = new Uri(baseUrl);
});
- services.AddTransient();
- //services.AddSingleton();
- //services.AddSingleton(provider => new CouchClient(databaseName, couchUrl));
-
- services.AddControllers();
services
- .AddAuthentication(options =>
+ .AddControllers()
+ .AddNewtonsoftJson(options =>
+ {
+ options.SerializerSettings.Formatting = Formatting.Indented;
+ options.SerializerSettings.ContractResolver = new DefaultContractResolver
{
- options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
- options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
- })
- .AddJwtBearer(options =>
- {
- options.Authority = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0";
- options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122";
- options.TokenValidationParameters.ValidateIssuer = true;
- options.TokenValidationParameters.ValidateAudience = true;
+ NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true }
+ };
+ options.SerializerSettings.Converters = new[] { new StringEnumConverter() };
+ options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
+ });
- options.Events = new JwtBearerEvents
- {
- OnMessageReceived = (context) =>
- {
- if (context.HttpContext.WebSockets.IsWebSocketRequest)
- {
- Console.WriteLine("Yep");
- }
- return Task.FromResult(0);
- }
- };
- });
+ services.AddAuthentication("CookieOrJwt")
+ .AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options =>
+ {
+ options.ForwardDefaultSelector = context =>
+ {
+ var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
+ return bearerAuth
+ ? JwtBearerDefaults.AuthenticationScheme
+ : CookieAuthenticationDefaults.AuthenticationScheme;
+ };
+ })
+ .AddCookie(options =>
+ {
+ options.Cookie.Name = "session-id";
+ options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
+ 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 { { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" } },
+ Scheme = "Bearer"
+ });
+ config.PostProcess = document =>
+ {
+ document.Info.Title = "Gameboard.ShogiUI.Sockets";
+ };
+ });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -114,30 +126,45 @@ namespace Gameboard.ShogiUI.Sockets
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
+ var client = PublicClientApplicationBuilder
+ .Create(Configuration["AzureAd:ClientId"])
+ .WithLogging(
+ (level, message, pii) =>
+ {
+
+ },
+ LogLevel.Verbose,
+ true,
+ true
+ )
+ .Build();
}
else
{
app.UseHsts();
}
app
- .UseRequestResponseLogging()
- .UseCors(
- opt => opt
- .WithOrigins(origins)
- .AllowAnyMethod()
- .AllowAnyHeader()
- .WithExposedHeaders("Set-Cookie")
- .AllowCredentials()
- )
- .UseRouting()
- .UseAuthentication()
- .UseAuthorization()
- .UseWebSockets(socketOptions)
- .UseEndpoints(endpoints =>
+ .UseRequestResponseLogging()
+ .UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials())
+ .UseRouting()
+ .UseAuthentication()
+ .UseAuthorization()
+ .UseOpenApi()
+ .UseSwaggerUi3(config =>
+ {
+ config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings()
{
- endpoints.MapControllers();
- })
- .Use(async (context, next) =>
+ ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff",
+ UsePkceWithAuthorizationCodeGrant = true
+ };
+ //config.WithCredentials = true;
+ })
+ .UseWebSockets(socketOptions)
+ .UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ })
+ .Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
diff --git a/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs b/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs
index 8294dd6..c6831db 100644
--- a/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs
+++ b/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs
@@ -17,7 +17,6 @@ namespace Gameboard.ShogiUI.Sockets.Utilities
{
var file = (char)(x + A);
var rank = y + 1;
- Console.WriteLine($"({x},{y}) - {file}{rank}");
return $"{file}{rank}";
}
diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json
index 9d9f40f..060e229 100644
--- a/Gameboard.ShogiUI.Sockets/appsettings.json
+++ b/Gameboard.ShogiUI.Sockets/appsettings.json
@@ -12,5 +12,11 @@
"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": "*"
}
\ No newline at end of file
diff --git a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj
index a15e786..3848edc 100644
--- a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj
+++ b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj
@@ -6,10 +6,10 @@
-
-
-
-
+
+
+
+
diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj
index c86f2d1..d47800f 100644
--- a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj
+++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj
@@ -8,14 +8,14 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs
deleted file mode 100644
index 8541ea4..0000000
--- a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs
+++ /dev/null
@@ -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();
-
- // 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();
- }
- }
- }
-}
diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs
index e5ded9f..aa3f3b4 100644
--- a/PathFinding/PathFinder2D.cs
+++ b/PathFinding/PathFinder2D.cs
@@ -72,7 +72,6 @@ namespace PathFinding
var element = collection[from];
if (element == null)
{
- Console.WriteLine("Null element in PathEvery");
return;
}
foreach (var path in element.MoveSet.GetMoves())