checkpoint
This commit is contained in:
@@ -2,18 +2,16 @@
|
|||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||||
{
|
{
|
||||||
public class GetGuestToken
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetGuestTokenResponse
|
public class GetGuestTokenResponse
|
||||||
{
|
{
|
||||||
public string PlayerName { get; }
|
public string UserId { get; }
|
||||||
|
public string DisplayName { get; }
|
||||||
public Guid OneTimeToken { get; }
|
public Guid OneTimeToken { get; }
|
||||||
|
|
||||||
public GetGuestTokenResponse(string playerName, Guid token)
|
public GetGuestTokenResponse(string id, string displayName, Guid token)
|
||||||
{
|
{
|
||||||
PlayerName = playerName;
|
UserId = id;
|
||||||
|
DisplayName = displayName;
|
||||||
OneTimeToken = token;
|
OneTimeToken = token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||||
{
|
{
|
||||||
public class GetGameResponse
|
public class GetSessionResponse
|
||||||
{
|
{
|
||||||
public Game Game { get; set; }
|
public Game Game { get; set; }
|
||||||
public WhichPlayer PlayerPerspective { get; set; }
|
public WhichPlayer PlayerPerspective { get; set; }
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||||
|
{
|
||||||
|
public class GetSessionsResponse
|
||||||
|
{
|
||||||
|
public Collection<Game> PlayerHasJoinedSessions { get; set; }
|
||||||
|
public Collection<Game> AllOtherSessions { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,14 +17,15 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|||||||
public class JoinGameResponse : IResponse
|
public class JoinGameResponse : IResponse
|
||||||
{
|
{
|
||||||
public string Action { get; protected set; }
|
public string Action { get; protected set; }
|
||||||
public string Error { get; set; }
|
|
||||||
public string GameName { get; set; }
|
public string GameName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The player who joined the game.
|
||||||
|
/// </summary>
|
||||||
public string PlayerName { get; set; }
|
public string PlayerName { get; set; }
|
||||||
|
|
||||||
public JoinGameResponse()
|
public JoinGameResponse()
|
||||||
{
|
{
|
||||||
Action = ClientAction.JoinGame.ToString();
|
Action = ClientAction.JoinGame.ToString();
|
||||||
Error = "";
|
|
||||||
GameName = "";
|
GameName = "";
|
||||||
PlayerName = "";
|
PlayerName = "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||||
using System.Collections.Generic;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||||
{
|
{
|
||||||
public class MoveResponse : IResponse
|
public class MoveResponse : IResponse
|
||||||
{
|
{
|
||||||
public string Action { get; protected set; }
|
public string Action { get; }
|
||||||
public Game Game { get; set; }
|
public string GameName { get; set; }
|
||||||
public WhichPlayer PlayerPerspective { get; set; }
|
/// <summary>
|
||||||
public BoardState BoardState { get; set; }
|
/// The player that made the move.
|
||||||
public IList<Move> MoveHistory { get; set; }
|
/// </summary>
|
||||||
|
public string PlayerName { get; set; }
|
||||||
|
|
||||||
public MoveResponse()
|
public MoveResponse()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
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
|
||||||
@@ -71,10 +74,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
{
|
{
|
||||||
var user = await gameboardManager.ReadUser(User);
|
var user = await gameboardManager.ReadUser(User);
|
||||||
var session = await gameboardRepository.ReadSession(gameName);
|
var session = await gameboardRepository.ReadSession(gameName);
|
||||||
|
if (session == null)
|
||||||
if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name))
|
|
||||||
{
|
{
|
||||||
throw new UnauthorizedAccessException("User is not seated at this game.");
|
return NotFound();
|
||||||
|
}
|
||||||
|
if (user == null || (session.Player1.Id != user.Id && session.Player2?.Id != user.Id))
|
||||||
|
{
|
||||||
|
return Forbid("User is not seated at this game.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var move = request.Move;
|
var move = request.Move;
|
||||||
@@ -92,15 +98,14 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
}
|
}
|
||||||
await communicationManager.BroadcastToPlayers(new MoveResponse
|
await communicationManager.BroadcastToPlayers(new MoveResponse
|
||||||
{
|
{
|
||||||
BoardState = session.Shogi.ToServiceModel(),
|
GameName = session.Name,
|
||||||
Game = session.ToServiceModel(),
|
PlayerName = user.Id
|
||||||
MoveHistory = session.Shogi.MoveHistory.Select(h => h.ToServiceModel()).ToList(),
|
}, session.Player1.Id, session.Player2?.Id);
|
||||||
PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2
|
|
||||||
}, session.Player1, session.Player2);
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
throw new InvalidOperationException("Illegal move.");
|
return Conflict("Illegal move.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
|
// TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
|
||||||
//[Route("")]
|
//[Route("")]
|
||||||
//public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
//public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||||
@@ -125,8 +130,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||||
{
|
{
|
||||||
var user = await gameboardManager.ReadUser(User);
|
var user = await ReadUserOrThrow();
|
||||||
var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!.Name);
|
var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!);
|
||||||
var success = await gameboardRepository.CreateSession(session);
|
var success = await gameboardRepository.CreateSession(session);
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
@@ -134,7 +139,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
await communicationManager.BroadcastToAll(new CreateGameResponse
|
await communicationManager.BroadcastToAll(new CreateGameResponse
|
||||||
{
|
{
|
||||||
Game = session.ToServiceModel(),
|
Game = session.ToServiceModel(),
|
||||||
PlayerName = user.Name
|
PlayerName = user.Id
|
||||||
|
}).ContinueWith(cont =>
|
||||||
|
{
|
||||||
|
if (cont.Exception != null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Yep");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
@@ -148,29 +159,83 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
[HttpGet("{gameName}")]
|
[HttpGet("{gameName}")]
|
||||||
public async Task<IActionResult> GetSession([FromRoute] string gameName)
|
public async Task<IActionResult> GetSession([FromRoute] string gameName)
|
||||||
{
|
{
|
||||||
var user = await gameboardManager.ReadUser(User);
|
var user = await ReadUserOrThrow();
|
||||||
var session = await gameboardRepository.ReadSession(gameName);
|
var session = await gameboardRepository.ReadSession(gameName);
|
||||||
if (session == null)
|
if (session == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
communicationManager.SubscribeToGame(session, user!.Name);
|
communicationManager.SubscribeToGame(session, user!.Id);
|
||||||
var response = new GetGameResponse()
|
var response = new GetSessionResponse()
|
||||||
{
|
{
|
||||||
Game = new Models.SessionMetadata(session).ToServiceModel(),
|
Game = new Models.SessionMetadata(session).ToServiceModel(user),
|
||||||
BoardState = session.Shogi.ToServiceModel(),
|
BoardState = session.Shogi.ToServiceModel(),
|
||||||
MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(),
|
MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(),
|
||||||
PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2
|
PlayerPerspective = user.Id == session.Player1.Id ? WhichPlayer.Player1 : WhichPlayer.Player2
|
||||||
};
|
};
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetSessions()
|
public async Task<GetSessionsResponse> GetSessions()
|
||||||
{
|
{
|
||||||
|
var user = await ReadUserOrThrow();
|
||||||
var sessions = await gameboardRepository.ReadSessionMetadatas();
|
var sessions = await gameboardRepository.ReadSessionMetadatas();
|
||||||
return new JsonResult(sessions.Select(s => s.ToServiceModel()).ToList());
|
|
||||||
|
var sessionsJoinedByUser = sessions
|
||||||
|
.Where(s => s.IsSeated(user))
|
||||||
|
.Select(s => s.ToServiceModel())
|
||||||
|
.ToList();
|
||||||
|
var sessionsNotJoinedByUser = sessions
|
||||||
|
.Where(s => !s.IsSeated(user))
|
||||||
|
.Select(s => s.ToServiceModel())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new GetSessionsResponse
|
||||||
|
{
|
||||||
|
PlayerHasJoinedSessions = new Collection<Game>(sessionsJoinedByUser),
|
||||||
|
AllOtherSessions = new Collection<Game>(sessionsNotJoinedByUser)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{gameName}")]
|
||||||
|
public async Task<IActionResult> PutJoinSession([FromRoute] string gameName)
|
||||||
|
{
|
||||||
|
var user = await ReadUserOrThrow();
|
||||||
|
var session = await gameboardRepository.ReadSessionMetaData(gameName);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
if (session.Player2 != null)
|
||||||
|
{
|
||||||
|
return this.Conflict("This session already has two seated players and is full.");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.SetPlayer2(user);
|
||||||
|
var success = await gameboardRepository.UpdateSession(session);
|
||||||
|
if (!success) return this.Problem(detail: "Unable to update session.");
|
||||||
|
|
||||||
|
var opponentName = user.Id == session.Player1.Id
|
||||||
|
? session.Player2!.Id
|
||||||
|
: session.Player1.Id;
|
||||||
|
await communicationManager.BroadcastToPlayers(new JoinGameResponse
|
||||||
|
{
|
||||||
|
GameName = session.Name,
|
||||||
|
PlayerName = user.Id
|
||||||
|
}, opponentName);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Models.User> ReadUserOrThrow()
|
||||||
|
{
|
||||||
|
var user = await gameboardManager.ReadUser(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("Unknown user claims.");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,24 +54,21 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
[HttpGet("Token")]
|
[HttpGet("Token")]
|
||||||
public async Task<IActionResult> GetToken()
|
public async Task<IActionResult> GetToken()
|
||||||
{
|
{
|
||||||
var identityId = User.UserId();
|
|
||||||
if (string.IsNullOrWhiteSpace(identityId))
|
|
||||||
{
|
|
||||||
return Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await gameboardManager.ReadUser(User);
|
var user = await gameboardManager.ReadUser(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
user = new User(identityId);
|
if (await gameboardManager.CreateUser(User))
|
||||||
var success = await gameboardRepository.CreateUser(user);
|
|
||||||
if (!success)
|
|
||||||
{
|
{
|
||||||
return Unauthorized();
|
user = await gameboardManager.ReadUser(User);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = tokenCache.GenerateToken(user.Name);
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = tokenCache.GenerateToken(user.Id);
|
||||||
return new JsonResult(new GetTokenResponse(token));
|
return new JsonResult(new GetTokenResponse(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,35 +76,28 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> GetGuestToken()
|
public async Task<IActionResult> GetGuestToken()
|
||||||
{
|
{
|
||||||
if (Guid.TryParse(User.UserId(), out Guid webSessionId))
|
var user = await gameboardManager.ReadUser(User);
|
||||||
|
if (user == null)
|
||||||
{
|
{
|
||||||
var user = await gameboardRepository.ReadGuestUser(webSessionId);
|
// Create a guest user.
|
||||||
if (user != null)
|
var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString());
|
||||||
|
var success = await gameboardRepository.CreateUser(newUser);
|
||||||
|
if (!success)
|
||||||
{
|
{
|
||||||
var token = tokenCache.GenerateToken(webSessionId.ToString());
|
return Conflict();
|
||||||
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
var identity = newUser.CreateClaimsIdentity();
|
||||||
{
|
await HttpContext.SignInAsync(
|
||||||
// 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,
|
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
new ClaimsPrincipal(identity),
|
new ClaimsPrincipal(identity),
|
||||||
authenticationProps
|
authenticationProps
|
||||||
);
|
);
|
||||||
|
user = newUser;
|
||||||
var token = tokenCache.GenerateToken(newSessionId.ToString());
|
|
||||||
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Unauthorized();
|
var token = tokenCache.GenerateToken(user.Id.ToString());
|
||||||
|
return this.Ok(new GetGuestTokenResponse(user.Id, user.DisplayName, token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,19 @@ namespace Gameboard.ShogiUI.Sockets.Extensions
|
|||||||
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string? DisplayName(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsGuest(this ClaimsPrincipal self)
|
public static bool IsGuest(this ClaimsPrincipal self)
|
||||||
{
|
{
|
||||||
return self.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Guest");
|
return self.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Guest");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ToCamelCase(this string self)
|
||||||
|
{
|
||||||
|
return char.ToLowerInvariant(self[0]) + self[1..];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
|
||||||
{
|
|
||||||
public interface IJoinGameHandler
|
|
||||||
{
|
|
||||||
Task Handle(JoinGameRequest request, string userName);
|
|
||||||
}
|
|
||||||
public class JoinGameHandler : IJoinGameHandler
|
|
||||||
{
|
|
||||||
private readonly IGameboardManager gameboardManager;
|
|
||||||
private readonly ISocketConnectionManager connectionManager;
|
|
||||||
public JoinGameHandler(
|
|
||||||
ISocketConnectionManager communicationManager,
|
|
||||||
IGameboardManager gameboardManager)
|
|
||||||
{
|
|
||||||
this.gameboardManager = gameboardManager;
|
|
||||||
this.connectionManager = communicationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(JoinGameRequest request, string userName)
|
|
||||||
{
|
|
||||||
var joinSucceeded = await gameboardManager.AssignPlayer2ToSession(request.GameName, userName);
|
|
||||||
|
|
||||||
var response = new JoinGameResponse()
|
|
||||||
{
|
|
||||||
PlayerName = userName,
|
|
||||||
GameName = request.GameName
|
|
||||||
};
|
|
||||||
if (joinSucceeded)
|
|
||||||
{
|
|
||||||
await connectionManager.BroadcastToAll(response);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
response.Error = "Game is full or does not exist.";
|
|
||||||
await connectionManager.BroadcastToPlayers(response, userName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
{
|
{
|
||||||
public interface IGameboardManager
|
public interface IGameboardManager
|
||||||
{
|
{
|
||||||
Task<bool> IsPlayer1(string sessionName, string playerName);
|
Task<bool> AssignPlayer2ToSession(string sessionName, User user);
|
||||||
Task<bool> AssignPlayer2ToSession(string sessionName, string userName);
|
|
||||||
Task<User?> ReadUser(ClaimsPrincipal user);
|
Task<User?> ReadUser(ClaimsPrincipal user);
|
||||||
|
Task<bool> CreateUser(ClaimsPrincipal user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GameboardManager : IGameboardManager
|
public class GameboardManager : IGameboardManager
|
||||||
@@ -23,14 +23,25 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<User?> ReadUser(ClaimsPrincipal user)
|
public Task<bool> CreateUser(ClaimsPrincipal principal)
|
||||||
{
|
{
|
||||||
var userId = user.UserId();
|
var id = principal.UserId();
|
||||||
if (user.IsGuest() && Guid.TryParse(userId, out var webSessionId))
|
if (string.IsNullOrEmpty(id))
|
||||||
{
|
{
|
||||||
return repository.ReadGuestUser(webSessionId);
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrEmpty(userId))
|
|
||||||
|
var user = principal.IsGuest()
|
||||||
|
? User.CreateGuestUser(id)
|
||||||
|
: User.CreateMsalUser(id);
|
||||||
|
|
||||||
|
return repository.CreateUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<User?> ReadUser(ClaimsPrincipal principal)
|
||||||
|
{
|
||||||
|
var userId = principal.UserId();
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
{
|
{
|
||||||
return repository.ReadUser(userId);
|
return repository.ReadUser(userId);
|
||||||
}
|
}
|
||||||
@@ -38,12 +49,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
return Task.FromResult<User?>(null);
|
return Task.FromResult<User?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsPlayer1(string sessionName, string playerName)
|
|
||||||
{
|
|
||||||
//var session = await repository.GetGame(sessionName);
|
|
||||||
//return session?.Player1 == playerName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> CreateJoinCode(string sessionName, string playerName)
|
public async Task<string> CreateJoinCode(string sessionName, string playerName)
|
||||||
{
|
{
|
||||||
@@ -55,19 +60,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AssignPlayer2ToSession(string sessionName, string userName)
|
public async Task<bool> AssignPlayer2ToSession(string sessionName, User user)
|
||||||
{
|
{
|
||||||
var isSuccess = false;
|
|
||||||
var session = await repository.ReadSessionMetaData(sessionName);
|
var session = await repository.ReadSessionMetaData(sessionName);
|
||||||
if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2))
|
if (session != null && !session.IsPrivate && session.Player2 == null)
|
||||||
{
|
{
|
||||||
session.SetPlayer2(userName);
|
session.SetPlayer2(user);
|
||||||
if (await repository.UpdateSession(session))
|
return await repository.UpdateSession(session);
|
||||||
{
|
|
||||||
isSuccess = true;
|
|
||||||
}
|
}
|
||||||
}
|
return false;
|
||||||
return isSuccess;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Gameboard.ShogiUI.Sockets.Models;
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
@@ -106,9 +107,32 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
foreach (var kvp in connections)
|
foreach (var kvp in connections)
|
||||||
{
|
{
|
||||||
var socket = kvp.Value;
|
var socket = kvp.Value;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
tasks.Add(socket.SendTextAsync(message));
|
tasks.Add(socket.SendTextAsync(message));
|
||||||
}
|
}
|
||||||
return Task.WhenAll(tasks);
|
catch (WebSocketException webSocketException)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
||||||
|
UnsubscribeFromBroadcastAndGames(kvp.Key);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
||||||
|
UnsubscribeFromBroadcastAndGames(kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var task = Task.WhenAll(tasks);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Yo");
|
||||||
|
}
|
||||||
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
//public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2)
|
//public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2)
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ namespace Gameboard.ShogiUI.Sockets.Models
|
|||||||
// TODO: Separate subscriptions to the Session from the Session.
|
// TODO: Separate subscriptions to the Session from the Session.
|
||||||
[JsonIgnore] public ConcurrentDictionary<string, WebSocket> Subscriptions { get; }
|
[JsonIgnore] public ConcurrentDictionary<string, WebSocket> Subscriptions { get; }
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public string Player1 { get; }
|
public User Player1 { get; }
|
||||||
public string? Player2 { get; private set; }
|
public User? Player2 { get; private set; }
|
||||||
public bool IsPrivate { get; }
|
public bool IsPrivate { get; }
|
||||||
|
|
||||||
|
// TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied.
|
||||||
public Shogi Shogi { get; }
|
public Shogi Shogi { get; }
|
||||||
|
|
||||||
public Session(string name, bool isPrivate, Shogi shogi, string player1, string? player2 = null)
|
public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null)
|
||||||
{
|
{
|
||||||
Subscriptions = new ConcurrentDictionary<string, WebSocket>();
|
Subscriptions = new ConcurrentDictionary<string, WebSocket>();
|
||||||
|
|
||||||
@@ -27,11 +28,11 @@ namespace Gameboard.ShogiUI.Sockets.Models
|
|||||||
Shogi = shogi;
|
Shogi = shogi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetPlayer2(string userName)
|
public void SetPlayer2(User user)
|
||||||
{
|
{
|
||||||
Player2 = userName;
|
Player2 = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1, Player2 = Player2 };
|
public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1.DisplayName, Player2 = Player2?.DisplayName };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
public class SessionMetadata
|
public class SessionMetadata
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public string Player1 { get; }
|
public User Player1 { get; }
|
||||||
public string? Player2 { get; private set; }
|
public User? Player2 { get; private set; }
|
||||||
public bool IsPrivate { get; }
|
public bool IsPrivate { get; }
|
||||||
|
|
||||||
public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null)
|
public SessionMetadata(string name, bool isPrivate, User player1, User? player2 = null)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
IsPrivate = isPrivate;
|
IsPrivate = isPrivate;
|
||||||
@@ -25,11 +25,27 @@
|
|||||||
Player2 = sessionModel.Player2;
|
Player2 = sessionModel.Player2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetPlayer2(string playerName)
|
public void SetPlayer2(User user)
|
||||||
{
|
{
|
||||||
Player2 = playerName;
|
Player2 = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1, Player2);
|
public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id;
|
||||||
|
|
||||||
|
public ServiceModels.Types.Game ToServiceModel(User? user = null)
|
||||||
|
{
|
||||||
|
// TODO: Find a better way for the UI to know whether or not they are seated at a given game than client-side ID matching.
|
||||||
|
var player1 = Player1.DisplayName;
|
||||||
|
var player2 = Player2?.DisplayName;
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
if (user.Id == Player1.Id) player1 = Player1.Id;
|
||||||
|
if (Player2 != null && user.Id == Player2.Id)
|
||||||
|
{
|
||||||
|
player2 = Player2.DisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new(Name, player1, player2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,79 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Models
|
namespace Gameboard.ShogiUI.Sockets.Models
|
||||||
{
|
{
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public static readonly ReadOnlyCollection<string> Adjectives = new(new[] {
|
||||||
public Guid? WebSessionId { get; }
|
"Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly"
|
||||||
|
});
|
||||||
public bool IsGuest => WebSessionId.HasValue;
|
public static readonly ReadOnlyCollection<string> Subjects = new(new[] {
|
||||||
|
"Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato"
|
||||||
public User(string name)
|
});
|
||||||
|
public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft);
|
||||||
|
public static User CreateGuestUser(string id)
|
||||||
{
|
{
|
||||||
Name = name;
|
var random = new Random();
|
||||||
|
// Adjective
|
||||||
|
var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count);
|
||||||
|
var adj = Adjectives[index];
|
||||||
|
// Subject
|
||||||
|
index = (int)Math.Floor(random.NextDouble() * Subjects.Count);
|
||||||
|
var subj = Subjects[index];
|
||||||
|
|
||||||
|
return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public string Id { get; }
|
||||||
/// Constructor for guest user.
|
public string DisplayName { get; }
|
||||||
/// </summary>
|
|
||||||
public User(string name, Guid webSessionId)
|
public WhichLoginPlatform LoginPlatform { get; }
|
||||||
|
|
||||||
|
public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest;
|
||||||
|
|
||||||
|
public User(string id, string displayName, WhichLoginPlatform platform)
|
||||||
{
|
{
|
||||||
Name = name;
|
Id = id;
|
||||||
WebSessionId = webSessionId;
|
DisplayName = displayName;
|
||||||
|
LoginPlatform = platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClaimsIdentity CreateMsalUserIdentity()
|
public User(UserDocument document)
|
||||||
{
|
{
|
||||||
var claims = new List<Claim>()
|
Id = document.Id;
|
||||||
{
|
DisplayName = document.DisplayName;
|
||||||
new Claim(ClaimTypes.NameIdentifier, Name),
|
LoginPlatform = document.Platform;
|
||||||
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
|
||||||
};
|
|
||||||
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClaimsIdentity CreateGuestUserIdentity()
|
public ClaimsIdentity CreateClaimsIdentity()
|
||||||
{
|
{
|
||||||
// TODO: Make this method static and factory-like.
|
if (LoginPlatform == WhichLoginPlatform.Guest)
|
||||||
if (!WebSessionId.HasValue)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Cannot create guest identity without a session identifier.");
|
var claims = new List<Claim>(4)
|
||||||
}
|
|
||||||
|
|
||||||
var claims = new List<Claim>()
|
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.NameIdentifier, WebSessionId.Value.ToString()),
|
new Claim(ClaimTypes.NameIdentifier, Id),
|
||||||
|
new Claim(ClaimTypes.Name, DisplayName),
|
||||||
new Claim(ClaimTypes.Role, "Guest"),
|
new Claim(ClaimTypes.Role, "Guest"),
|
||||||
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||||
};
|
};
|
||||||
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>(3)
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, Id),
|
||||||
|
new Claim(ClaimTypes.Name, DisplayName),
|
||||||
|
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||||
|
};
|
||||||
|
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
public enum WhichLoginPlatform
|
public enum WhichLoginPlatform
|
||||||
{
|
{
|
||||||
|
Unknown,
|
||||||
Microsoft,
|
Microsoft,
|
||||||
Guest
|
Guest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Gameboard.ShogiUI.Sockets.Utilities;
|
||||||
using Gameboard.ShogiUI.Sockets.Utilities;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
public abstract class CouchDocument
|
public abstract class CouchDocument
|
||||||
{
|
{
|
||||||
[JsonProperty("_id")] public string Id { get; set; }
|
[JsonProperty("_id")] public string Id { get; set; }
|
||||||
|
[JsonProperty("_rev")] public string? RevisionId { get; set; }
|
||||||
public WhichDocumentType DocumentType { get; }
|
public WhichDocumentType DocumentType { get; }
|
||||||
public DateTimeOffset CreatedDate { get; set; }
|
public DateTimeOffset CreatedDate { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
|
{
|
||||||
|
public class CouchViewResult<T> where T : class
|
||||||
|
{
|
||||||
|
public int total_rows;
|
||||||
|
public int offset;
|
||||||
|
public CouchViewResultRow<T>[] rows;
|
||||||
|
|
||||||
|
public CouchViewResult()
|
||||||
|
{
|
||||||
|
rows = Array.Empty<CouchViewResultRow<T>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CouchViewResultRow<T>
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public T doc;
|
||||||
|
|
||||||
|
public CouchViewResultRow()
|
||||||
|
{
|
||||||
|
id = string.Empty;
|
||||||
|
doc = default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
public class SessionDocument : CouchDocument
|
public class SessionDocument : CouchDocument
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Player1 { get; set; }
|
public string Player1Id { get; set; }
|
||||||
public string? Player2 { get; set; }
|
public string? Player2Id { get; set; }
|
||||||
public bool IsPrivate { get; set; }
|
public bool IsPrivate { get; set; }
|
||||||
public IList<BoardStateDocument> History { get; set; }
|
public IList<BoardStateDocument> History { get; set; }
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
public SessionDocument() : base(WhichDocumentType.Session)
|
public SessionDocument() : base(WhichDocumentType.Session)
|
||||||
{
|
{
|
||||||
Name = string.Empty;
|
Name = string.Empty;
|
||||||
Player1 = string.Empty;
|
Player1Id = string.Empty;
|
||||||
Player2 = string.Empty;
|
Player2Id = string.Empty;
|
||||||
History = new List<BoardStateDocument>(0);
|
History = new List<BoardStateDocument>(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
: base(session.Name, WhichDocumentType.Session)
|
: base(session.Name, WhichDocumentType.Session)
|
||||||
{
|
{
|
||||||
Name = session.Name;
|
Name = session.Name;
|
||||||
Player1 = session.Player1;
|
Player1Id = session.Player1.Id;
|
||||||
Player2 = session.Player2;
|
Player2Id = session.Player2?.Id;
|
||||||
IsPrivate = session.IsPrivate;
|
IsPrivate = session.IsPrivate;
|
||||||
History = new List<BoardStateDocument>(0);
|
History = new List<BoardStateDocument>(0);
|
||||||
}
|
}
|
||||||
@@ -35,14 +35,10 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
: base(sessionMetaData.Name, WhichDocumentType.Session)
|
: base(sessionMetaData.Name, WhichDocumentType.Session)
|
||||||
{
|
{
|
||||||
Name = sessionMetaData.Name;
|
Name = sessionMetaData.Name;
|
||||||
Player1 = sessionMetaData.Player1;
|
Player1Id = sessionMetaData.Player1.Id;
|
||||||
Player2 = sessionMetaData.Player2;
|
Player2Id = sessionMetaData.Player2?.Id;
|
||||||
IsPrivate = sessionMetaData.IsPrivate;
|
IsPrivate = sessionMetaData.IsPrivate;
|
||||||
History = new List<BoardStateDocument>(0);
|
History = new List<BoardStateDocument>(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Models.Session ToDomainModel(Models.Shogi shogi) => new(Name, IsPrivate, shogi, Player1, Player2);
|
|
||||||
|
|
||||||
public Models.SessionMetadata ToDomainModel() => new(Name, IsPrivate, Player1, Player2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Models;
|
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 string DisplayName { get; set; }
|
||||||
public string Name { get; set; }
|
|
||||||
public WhichLoginPlatform Platform { get; set; }
|
public WhichLoginPlatform Platform { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// The browser session ID saved via Set-Cookie headers.
|
|
||||||
/// Only used with guest accounts.
|
|
||||||
/// </summary>
|
|
||||||
public Guid? WebSessionId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor for JSON deserializing.
|
/// Constructor for JSON deserializing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UserDocument() : base(WhichDocumentType.User)
|
public UserDocument() : base(WhichDocumentType.User)
|
||||||
{
|
{
|
||||||
Name = string.Empty;
|
DisplayName = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDocument(string name, Guid? webSessionId = null) : base($"org.couchdb.user:{name}", WhichDocumentType.User)
|
public UserDocument(
|
||||||
|
string id,
|
||||||
|
string displayName,
|
||||||
|
WhichLoginPlatform platform) : base(id, WhichDocumentType.User)
|
||||||
{
|
{
|
||||||
Name = name;
|
DisplayName = displayName;
|
||||||
WebSessionId = webSessionId;
|
Platform = platform;
|
||||||
Platform = WebSessionId.HasValue
|
|
||||||
? WhichLoginPlatform.Guest
|
|
||||||
: WhichLoginPlatform.Microsoft;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
|
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -16,10 +20,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
Task<bool> CreateBoardState(Models.Session session);
|
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<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
|
||||||
Task<Models.User?> ReadGuestUser(Guid webSessionId);
|
|
||||||
Task<Models.Session?> ReadSession(string name);
|
Task<Models.Session?> ReadSession(string name);
|
||||||
Task<Models.Shogi?> ReadShogi(string name);
|
|
||||||
Task<bool> UpdateSession(Models.SessionMetadata session);
|
Task<bool> UpdateSession(Models.SessionMetadata session);
|
||||||
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
|
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
|
||||||
Task<Models.User?> ReadUser(string userName);
|
Task<Models.User?> ReadUser(string userName);
|
||||||
@@ -27,6 +29,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
|
|
||||||
public class GameboardRepository : IGameboardRepository
|
public class GameboardRepository : IGameboardRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns session, board state, and user documents, grouped by session.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate";
|
||||||
|
/// <summary>
|
||||||
|
/// Returns session and user documents, grouped by session.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata";
|
||||||
|
private static readonly string View_User = "_design/user/_view/user";
|
||||||
private const string ApplicationJson = "application/json";
|
private const string ApplicationJson = "application/json";
|
||||||
private readonly HttpClient client;
|
private readonly HttpClient client;
|
||||||
private readonly ILogger<GameboardRepository> logger;
|
private readonly ILogger<GameboardRepository> logger;
|
||||||
@@ -37,87 +48,124 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<Models.SessionMetadata>> ReadSessionMetadatas()
|
public async Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas()
|
||||||
{
|
{
|
||||||
var selector = new Dictionary<string, object>(2)
|
var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
|
||||||
{
|
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
|
||||||
[nameof(SessionDocument.DocumentType)] = WhichDocumentType.Session
|
|
||||||
};
|
|
||||||
var q = new { Selector = selector };
|
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson);
|
|
||||||
var response = await client.PostAsync("_find", content);
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
var results = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent);
|
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||||
if (results != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
return results
|
var groupedBySession = result.rows.GroupBy(row => row.id);
|
||||||
.docs
|
var sessions = new List<Models.SessionMetadata>(result.total_rows / 3);
|
||||||
.Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
|
foreach (var group in groupedBySession)
|
||||||
.ToList();
|
{
|
||||||
|
/**
|
||||||
|
* A group contains 3 elements.
|
||||||
|
* 1) The session metadata.
|
||||||
|
* 2) User document of Player1.
|
||||||
|
* 3) User document of Player2.
|
||||||
|
*/
|
||||||
|
var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>();
|
||||||
|
var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>();
|
||||||
|
var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject<UserDocument>();
|
||||||
|
if (session != null && player1Doc != null)
|
||||||
|
{
|
||||||
|
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
|
||||||
|
sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return new List<Models.SessionMetadata>(0);
|
return new Collection<Models.SessionMetadata>(sessions);
|
||||||
|
}
|
||||||
|
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.Session?> ReadSession(string name)
|
public async Task<Models.Session?> ReadSession(string name)
|
||||||
{
|
{
|
||||||
var readShogiTask = ReadShogi(name);
|
var queryParams = new QueryBuilder
|
||||||
var response = await client.GetAsync(name);
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
|
||||||
var couchModel = JsonConvert.DeserializeObject<SessionDocument>(responseContent);
|
|
||||||
var shogi = await readShogiTask;
|
|
||||||
if (shogi == null)
|
|
||||||
{
|
{
|
||||||
return null;
|
{ "include_docs", "true" },
|
||||||
|
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
|
||||||
|
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
|
||||||
|
}.ToQueryString();
|
||||||
|
var query = $"{View_SessionWithBoardState}{queryParams}";
|
||||||
|
logger.LogInformation("ReadSession() query: {query}", query);
|
||||||
|
var response = await client.GetAsync(query);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||||
|
if (result != null && result.rows.Length > 2)
|
||||||
|
{
|
||||||
|
var group = result.rows;
|
||||||
|
/**
|
||||||
|
* A group contains 3 type of elements.
|
||||||
|
* 1) The session metadata.
|
||||||
|
* 2) User documents of Player1 and Player2.
|
||||||
|
* 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :(
|
||||||
|
* 3) BoardState
|
||||||
|
*/
|
||||||
|
var session = group[0].doc.ToObject<SessionDocument>();
|
||||||
|
var player1Doc = group[1].doc.ToObject<UserDocument>();
|
||||||
|
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
|
||||||
|
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
|
||||||
|
? group[2].doc.ToObject<UserDocument>()
|
||||||
|
: null;
|
||||||
|
var moves = group
|
||||||
|
.Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session.
|
||||||
|
// TODO: Deserialize just the Move property.
|
||||||
|
.Select(row => row.doc.ToObject<BoardStateDocument>())
|
||||||
|
.Select(boardState =>
|
||||||
|
{
|
||||||
|
var move = boardState!.Move!;
|
||||||
|
return move.PieceFromHand.HasValue
|
||||||
|
? new Models.Move(move.PieceFromHand.Value, move.To)
|
||||||
|
: new Models.Move(move.From!, move.To, move.IsPromotion);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var shogi = new Models.Shogi(moves);
|
||||||
|
if (session != null && player1Doc != null)
|
||||||
|
{
|
||||||
|
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
|
||||||
|
return new Models.Session(session.Name, session.IsPrivate, shogi, new(player1Doc), player2);
|
||||||
}
|
}
|
||||||
return couchModel.ToDomainModel(shogi);
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name)
|
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name)
|
||||||
{
|
{
|
||||||
var response = await client.GetAsync(name);
|
var queryParams = new QueryBuilder
|
||||||
|
{
|
||||||
|
{ "include_docs", "true" },
|
||||||
|
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
|
||||||
|
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
|
||||||
|
}.ToQueryString();
|
||||||
|
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
var couchModel = JsonConvert.DeserializeObject<SessionDocument>(responseContent);
|
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||||
return couchModel.ToDomainModel();
|
if (result != null && result.rows.Length > 2)
|
||||||
|
{
|
||||||
|
var group = result.rows;
|
||||||
|
/**
|
||||||
|
* A group contains 3 elements.
|
||||||
|
* 1) The session metadata.
|
||||||
|
* 2) User document of Player1.
|
||||||
|
* 3) User document of Player2.
|
||||||
|
*/
|
||||||
|
var session = group[0].doc.ToObject<SessionDocument>();
|
||||||
|
var player1Doc = group[1].doc.ToObject<UserDocument>();
|
||||||
|
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
|
||||||
|
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
|
||||||
|
? group[2].doc.ToObject<UserDocument>()
|
||||||
|
: null;
|
||||||
|
if (session != null && player1Doc != null)
|
||||||
|
{
|
||||||
|
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
|
||||||
|
return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.Shogi?> ReadShogi(string name)
|
|
||||||
{
|
|
||||||
var selector = new Dictionary<string, object>(2)
|
|
||||||
{
|
|
||||||
[nameof(BoardStateDocument.DocumentType)] = WhichDocumentType.BoardState,
|
|
||||||
[nameof(BoardStateDocument.Name)] = name
|
|
||||||
};
|
|
||||||
var sort = new Dictionary<string, object>(1)
|
|
||||||
{
|
|
||||||
[nameof(BoardStateDocument.CreatedDate)] = "asc"
|
|
||||||
};
|
|
||||||
var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } });
|
|
||||||
|
|
||||||
var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
|
|
||||||
var response = await client.PostAsync("_find", content);
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadShogi), responseContent, query);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var boardStates = JsonConvert
|
|
||||||
.DeserializeObject<CouchFindResult<BoardStateDocument>>(responseContent)
|
|
||||||
.docs;
|
|
||||||
if (boardStates.Length == 0) return null;
|
|
||||||
|
|
||||||
// Skip(1) because the first BoardState has no move; it represents the initial board state of a new Session.
|
|
||||||
var moves = boardStates.Skip(1).Select(couchModel =>
|
|
||||||
{
|
|
||||||
var move = couchModel.Move;
|
|
||||||
Models.Move model = move!.PieceFromHand.HasValue
|
|
||||||
? new Models.Move(move.PieceFromHand.Value, move.To)
|
|
||||||
: new Models.Move(move.From!, move.To, move.IsPromotion);
|
|
||||||
return model;
|
|
||||||
}).ToList();
|
|
||||||
return new Models.Shogi(moves);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves a snapshot of board state and the most recent move.
|
/// Saves a snapshot of board state and the most recent move.
|
||||||
@@ -149,7 +197,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
|
|
||||||
public async Task<bool> UpdateSession(Models.SessionMetadata session)
|
public async Task<bool> UpdateSession(Models.SessionMetadata session)
|
||||||
{
|
{
|
||||||
var couchModel = new SessionDocument(session);
|
// GET existing session to get revisionId.
|
||||||
|
var readResponse = await client.GetAsync(session.Name);
|
||||||
|
if (!readResponse.IsSuccessStatusCode) return false;
|
||||||
|
var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync());
|
||||||
|
|
||||||
|
// PUT the document with the revisionId.
|
||||||
|
var couchModel = new SessionDocument(session)
|
||||||
|
{
|
||||||
|
RevisionId = sessionDocument?.RevisionId
|
||||||
|
};
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
|
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
|
||||||
var response = await client.PutAsync(couchModel.Id, content);
|
var response = await client.PutAsync(couchModel.Id, content);
|
||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
@@ -205,66 +262,31 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.User?> ReadUser(string userName)
|
public async Task<Models.User?> ReadUser(string id)
|
||||||
{
|
{
|
||||||
try
|
var queryParams = new QueryBuilder
|
||||||
{
|
{
|
||||||
var document = new UserDocument(userName);
|
{ "include_docs", "true" },
|
||||||
var uri = new Uri(client.BaseAddress!, HttpUtility.UrlEncode(document.Id));
|
{ "key", JsonConvert.SerializeObject(id) },
|
||||||
var response = await client.GetAsync(HttpUtility.UrlEncode(document.Id));
|
}.ToQueryString();
|
||||||
var response2 = await client.GetAsync(uri);
|
var response = await client.GetAsync($"{View_User}{queryParams}");
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
if (response.IsSuccessStatusCode)
|
var result = JsonConvert.DeserializeObject<CouchViewResult<UserDocument>>(responseContent);
|
||||||
|
if (result != null && result.rows.Length > 0)
|
||||||
{
|
{
|
||||||
var user = JsonConvert.DeserializeObject<UserDocument>(responseContent);
|
return new Models.User(result.rows[0].doc);
|
||||||
|
}
|
||||||
|
|
||||||
return new Models.User(user.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CreateUser(Models.User user)
|
public async Task<bool> CreateUser(Models.User user)
|
||||||
{
|
{
|
||||||
var couchModel = new UserDocument(user.Name, user.WebSessionId);
|
var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
|
var content = new StringContent(JsonConvert.SerializeObject(couchModel), 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.User?> ReadGuestUser(Guid webSessionId)
|
|
||||||
{
|
|
||||||
var selector = new Dictionary<string, object>(2)
|
|
||||||
{
|
|
||||||
[nameof(UserDocument.DocumentType)] = WhichDocumentType.User,
|
|
||||||
[nameof(UserDocument.WebSessionId)] = webSessionId.ToString()
|
|
||||||
};
|
|
||||||
var query = JsonConvert.SerializeObject(new { selector, limit = 1 });
|
|
||||||
var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
|
|
||||||
var response = await client.PostAsync("_find", content);
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadGuestUser), responseContent, query);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = JsonConvert.DeserializeObject<CouchFindResult<UserDocument>>(responseContent);
|
|
||||||
if (!string.IsNullOrWhiteSpace(result.warning))
|
|
||||||
{
|
|
||||||
logger.LogError(new InvalidOperationException(result.warning), result.warning);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var userDocument = result.docs.SingleOrDefault();
|
|
||||||
if (userDocument != null)
|
|
||||||
{
|
|
||||||
return new Models.User(userDocument.Name);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using Gameboard.ShogiUI.Sockets.Controllers;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
using Gameboard.ShogiUI.Sockets.Managers;
|
||||||
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
|
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
|
||||||
@@ -34,7 +33,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
private readonly IGameboardManager gameboardManager;
|
private readonly IGameboardManager gameboardManager;
|
||||||
private readonly ISocketTokenCache tokenManager;
|
private readonly ISocketTokenCache tokenManager;
|
||||||
private readonly IJoinByCodeHandler joinByCodeHandler;
|
private readonly IJoinByCodeHandler joinByCodeHandler;
|
||||||
private readonly IJoinGameHandler joinGameHandler;
|
|
||||||
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
|
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
|
||||||
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
|
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
|
||||||
|
|
||||||
@@ -45,7 +43,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
IGameboardManager gameboardManager,
|
IGameboardManager gameboardManager,
|
||||||
ISocketTokenCache tokenManager,
|
ISocketTokenCache tokenManager,
|
||||||
IJoinByCodeHandler joinByCodeHandler,
|
IJoinByCodeHandler joinByCodeHandler,
|
||||||
IJoinGameHandler joinGameHandler,
|
|
||||||
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
|
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
|
||||||
IValidator<JoinGameRequest> joinGameRequestValidator
|
IValidator<JoinGameRequest> joinGameRequestValidator
|
||||||
) : base()
|
) : base()
|
||||||
@@ -56,34 +53,25 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
this.gameboardManager = gameboardManager;
|
this.gameboardManager = gameboardManager;
|
||||||
this.tokenManager = tokenManager;
|
this.tokenManager = tokenManager;
|
||||||
this.joinByCodeHandler = joinByCodeHandler;
|
this.joinByCodeHandler = joinByCodeHandler;
|
||||||
this.joinGameHandler = joinGameHandler;
|
|
||||||
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
|
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
|
||||||
this.joinGameRequestValidator = joinGameRequestValidator;
|
this.joinGameRequestValidator = joinGameRequestValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleSocketRequest(HttpContext context)
|
public async Task HandleSocketRequest(HttpContext context)
|
||||||
{
|
{
|
||||||
string? userName = null;
|
if (!context.Request.Query.Keys.Contains("token"))
|
||||||
var user = await gameboardManager.ReadUser(context.User);
|
|
||||||
if (user?.WebSessionId != null)
|
|
||||||
{
|
{
|
||||||
// Guest account
|
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||||
userName = tokenManager.GetUsername(user.WebSessionId.Value);
|
return;
|
||||||
}
|
}
|
||||||
else if (context.Request.Query.Keys.Contains("token"))
|
|
||||||
{
|
|
||||||
// Microsoft account
|
|
||||||
var token = Guid.Parse(context.Request.Query["token"][0]);
|
var token = Guid.Parse(context.Request.Query["token"][0]);
|
||||||
userName = tokenManager.GetUsername(token);
|
var userName = tokenManager.GetUsername(token);
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userName))
|
if (string.IsNullOrEmpty(userName))
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
communicationManager.SubscribeToBroadcast(socket, userName);
|
communicationManager.SubscribeToBroadcast(socket, userName);
|
||||||
@@ -95,31 +83,25 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||||
logger.LogInformation("Request \n{0}\n", message);
|
logger.LogInformation("Request \n{0}\n", message);
|
||||||
var request = JsonConvert.DeserializeObject<Request>(message);
|
var request = JsonConvert.DeserializeObject<Request>(message);
|
||||||
if (!Enum.IsDefined(typeof(ClientAction), request.Action))
|
if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action))
|
||||||
{
|
{
|
||||||
await socket.SendTextAsync("Error: Action not recognized.");
|
await socket.SendTextAsync("Error: Action not recognized.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch (request.Action)
|
switch (request.Action)
|
||||||
{
|
{
|
||||||
case ClientAction.JoinGame:
|
|
||||||
{
|
|
||||||
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
|
|
||||||
if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req))
|
|
||||||
{
|
|
||||||
await joinGameHandler.Handle(req, userName);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ClientAction.JoinByCode:
|
case ClientAction.JoinByCode:
|
||||||
{
|
{
|
||||||
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
|
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
|
||||||
if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
|
if (req != null && await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
|
||||||
{
|
{
|
||||||
await joinByCodeHandler.Handle(req, userName);
|
await joinByCodeHandler.Handle(req, userName);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work.");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
@@ -132,9 +114,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
logger.LogInformation("Probably tried writing to a closed socket.");
|
logger.LogInformation("Probably tried writing to a closed socket.");
|
||||||
logger.LogError(ex.Message);
|
logger.LogError(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
|
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,11 +124,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
if (!results.IsValid)
|
if (!results.IsValid)
|
||||||
{
|
{
|
||||||
var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage));
|
var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage));
|
||||||
var message = JsonConvert.SerializeObject(new Response
|
await socket.SendTextAsync(errors);
|
||||||
{
|
|
||||||
Error = errors
|
|
||||||
});
|
|
||||||
await socket.SendTextAsync(message);
|
|
||||||
}
|
}
|
||||||
return results.IsValid;
|
return results.IsValid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ namespace Gameboard.ShogiUI.Sockets.Services.Utility
|
|||||||
public class Response : IResponse
|
public class Response : IResponse
|
||||||
{
|
{
|
||||||
public string Action { get; set; }
|
public string Action { get; set; }
|
||||||
public string Error { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ namespace Gameboard.ShogiUI.Sockets
|
|||||||
var user = await gameboardRepository.ReadUser(nameClaim.Value);
|
var user = await gameboardRepository.ReadUser(nameClaim.Value);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
var newUser = new Models.User(nameClaim.Value);
|
var newUser = Models.User.CreateMsalUser(nameClaim.Value);
|
||||||
var success = await gameboardRepository.CreateUser(newUser);
|
var success = await gameboardRepository.CreateUser(newUser);
|
||||||
if (success) user = newUser;
|
if (success) user = newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
return new ClaimsPrincipal(user.CreateMsalUserIdentity());
|
return new ClaimsPrincipal(user.CreateClaimsIdentity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return principal;
|
return principal;
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ 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.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Http;
|
||||||
using Microsoft.Identity.Client;
|
using Microsoft.Identity.Client;
|
||||||
using Microsoft.Identity.Web;
|
using Microsoft.Identity.Web;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -41,7 +43,6 @@ namespace Gameboard.ShogiUI.Sockets
|
|||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
|
services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
|
||||||
services.AddSingleton<IJoinGameHandler, JoinGameHandler>();
|
|
||||||
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
||||||
services.AddSingleton<ISocketTokenCache, SocketTokenCache>();
|
services.AddSingleton<ISocketTokenCache, SocketTokenCache>();
|
||||||
services.AddSingleton<IGameboardManager, GameboardManager>();
|
services.AddSingleton<IGameboardManager, GameboardManager>();
|
||||||
@@ -109,6 +110,9 @@ namespace Gameboard.ShogiUI.Sockets
|
|||||||
document.Info.Title = "Gameboard.ShogiUI.Sockets";
|
document.Info.Title = "Gameboard.ShogiUI.Sockets";
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove default HttpClient logging.
|
||||||
|
services.RemoveAll<IHttpMessageHandlerBuilderFilter>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Warning",
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AzureAd": {
|
"AzureAd": {
|
||||||
|
|||||||
Reference in New Issue
Block a user