checkpoint
This commit is contained in:
@@ -2,18 +2,16 @@
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||
{
|
||||
public class GetGuestToken
|
||||
{
|
||||
}
|
||||
|
||||
public class GetGuestTokenResponse
|
||||
{
|
||||
public string PlayerName { get; }
|
||||
public string UserId { get; }
|
||||
public string DisplayName { 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
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 class GetSessionResponse
|
||||
{
|
||||
public Game Game { 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 string Action { get; protected set; }
|
||||
public string Error { get; set; }
|
||||
public string GameName { get; set; }
|
||||
/// <summary>
|
||||
/// The player who joined the game.
|
||||
/// </summary>
|
||||
public string PlayerName { get; set; }
|
||||
|
||||
public JoinGameResponse()
|
||||
{
|
||||
Action = ClientAction.JoinGame.ToString();
|
||||
Error = "";
|
||||
GameName = "";
|
||||
PlayerName = "";
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using System.Collections.Generic;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||
{
|
||||
public class MoveResponse : IResponse
|
||||
{
|
||||
public string Action { get; protected set; }
|
||||
public Game Game { get; set; }
|
||||
public WhichPlayer PlayerPerspective { get; set; }
|
||||
public BoardState BoardState { get; set; }
|
||||
public IList<Move> MoveHistory { get; set; }
|
||||
public string Action { get; }
|
||||
public string GameName { get; set; }
|
||||
/// <summary>
|
||||
/// The player that made the move.
|
||||
/// </summary>
|
||||
public string PlayerName { get; set; }
|
||||
|
||||
public MoveResponse()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,10 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
@@ -71,10 +74,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
{
|
||||
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))
|
||||
if (session == null)
|
||||
{
|
||||
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;
|
||||
@@ -92,15 +98,14 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
}
|
||||
await communicationManager.BroadcastToPlayers(new MoveResponse
|
||||
{
|
||||
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);
|
||||
GameName = session.Name,
|
||||
PlayerName = user.Id
|
||||
}, session.Player1.Id, session.Player2?.Id);
|
||||
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.
|
||||
//[Route("")]
|
||||
//public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||
@@ -125,8 +130,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||
{
|
||||
var user = await gameboardManager.ReadUser(User);
|
||||
var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!.Name);
|
||||
var user = await ReadUserOrThrow();
|
||||
var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!);
|
||||
var success = await gameboardRepository.CreateSession(session);
|
||||
|
||||
if (success)
|
||||
@@ -134,7 +139,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
await communicationManager.BroadcastToAll(new CreateGameResponse
|
||||
{
|
||||
Game = session.ToServiceModel(),
|
||||
PlayerName = user.Name
|
||||
PlayerName = user.Id
|
||||
}).ContinueWith(cont =>
|
||||
{
|
||||
if (cont.Exception != null)
|
||||
{
|
||||
Console.Error.WriteLine("Yep");
|
||||
}
|
||||
});
|
||||
return Ok();
|
||||
}
|
||||
@@ -148,29 +159,83 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
[HttpGet("{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);
|
||||
if (session == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
communicationManager.SubscribeToGame(session, user!.Name);
|
||||
var response = new GetGameResponse()
|
||||
communicationManager.SubscribeToGame(session, user!.Id);
|
||||
var response = new GetSessionResponse()
|
||||
{
|
||||
Game = new Models.SessionMetadata(session).ToServiceModel(),
|
||||
Game = new Models.SessionMetadata(session).ToServiceModel(user),
|
||||
BoardState = session.Shogi.ToServiceModel(),
|
||||
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);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetSessions()
|
||||
public async Task<GetSessionsResponse> GetSessions()
|
||||
{
|
||||
var user = await ReadUserOrThrow();
|
||||
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")]
|
||||
public async Task<IActionResult> GetToken()
|
||||
{
|
||||
var identityId = User.UserId();
|
||||
if (string.IsNullOrWhiteSpace(identityId))
|
||||
var user = await gameboardManager.ReadUser(User);
|
||||
if (user == null)
|
||||
{
|
||||
if (await gameboardManager.CreateUser(User))
|
||||
{
|
||||
user = await gameboardManager.ReadUser(User);
|
||||
}
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
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);
|
||||
var token = tokenCache.GenerateToken(user.Id);
|
||||
return new JsonResult(new GetTokenResponse(token));
|
||||
}
|
||||
|
||||
@@ -79,35 +76,28 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
[AllowAnonymous]
|
||||
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);
|
||||
if (user != null)
|
||||
// Create a guest user.
|
||||
var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString());
|
||||
var success = await gameboardRepository.CreateUser(newUser);
|
||||
if (!success)
|
||||
{
|
||||
var token = tokenCache.GenerateToken(webSessionId.ToString());
|
||||
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
||||
return Conflict();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Setup a guest user.
|
||||
var newSessionId = Guid.NewGuid();
|
||||
var user = new User(Guid.NewGuid().ToString(), newSessionId);
|
||||
if (await gameboardRepository.CreateUser(user))
|
||||
{
|
||||
var identity = user.CreateGuestUserIdentity();
|
||||
await this.HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(identity),
|
||||
authenticationProps
|
||||
);
|
||||
|
||||
var token = tokenCache.GenerateToken(newSessionId.ToString());
|
||||
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
||||
}
|
||||
var identity = newUser.CreateClaimsIdentity();
|
||||
await HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(identity),
|
||||
authenticationProps
|
||||
);
|
||||
user = newUser;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static string? DisplayName(this ClaimsPrincipal self)
|
||||
{
|
||||
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||
}
|
||||
|
||||
public static bool IsGuest(this ClaimsPrincipal self)
|
||||
{
|
||||
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
|
||||
{
|
||||
Task<bool> IsPlayer1(string sessionName, string playerName);
|
||||
Task<bool> AssignPlayer2ToSession(string sessionName, string userName);
|
||||
Task<bool> AssignPlayer2ToSession(string sessionName, User user);
|
||||
Task<User?> ReadUser(ClaimsPrincipal user);
|
||||
Task<bool> CreateUser(ClaimsPrincipal user);
|
||||
}
|
||||
|
||||
public class GameboardManager : IGameboardManager
|
||||
@@ -23,14 +23,25 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public Task<User?> ReadUser(ClaimsPrincipal user)
|
||||
public Task<bool> CreateUser(ClaimsPrincipal principal)
|
||||
{
|
||||
var userId = user.UserId();
|
||||
if (user.IsGuest() && Guid.TryParse(userId, out var webSessionId))
|
||||
var id = principal.UserId();
|
||||
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);
|
||||
}
|
||||
@@ -38,12 +49,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
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)
|
||||
{
|
||||
@@ -55,19 +60,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
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);
|
||||
if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2))
|
||||
if (session != null && !session.IsPrivate && session.Player2 == null)
|
||||
{
|
||||
session.SetPlayer2(userName);
|
||||
if (await repository.UpdateSession(session))
|
||||
{
|
||||
isSuccess = true;
|
||||
}
|
||||
session.SetPlayer2(user);
|
||||
return await repository.UpdateSession(session);
|
||||
}
|
||||
return isSuccess;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Gameboard.ShogiUI.Sockets.Models;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.WebSockets;
|
||||
@@ -106,9 +107,32 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
foreach (var kvp in connections)
|
||||
{
|
||||
var socket = kvp.Value;
|
||||
tasks.Add(socket.SendTextAsync(message));
|
||||
try
|
||||
{
|
||||
|
||||
tasks.Add(socket.SendTextAsync(message));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
return Task.WhenAll(tasks);
|
||||
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)
|
||||
|
||||
@@ -10,13 +10,14 @@ namespace Gameboard.ShogiUI.Sockets.Models
|
||||
// TODO: Separate subscriptions to the Session from the Session.
|
||||
[JsonIgnore] public ConcurrentDictionary<string, WebSocket> Subscriptions { get; }
|
||||
public string Name { get; }
|
||||
public string Player1 { get; }
|
||||
public string? Player2 { get; private set; }
|
||||
public User Player1 { get; }
|
||||
public User? Player2 { get; private set; }
|
||||
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 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>();
|
||||
|
||||
@@ -27,11 +28,11 @@ namespace Gameboard.ShogiUI.Sockets.Models
|
||||
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 string Name { get; }
|
||||
public string Player1 { get; }
|
||||
public string? Player2 { get; private set; }
|
||||
public User Player1 { get; }
|
||||
public User? Player2 { get; private set; }
|
||||
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;
|
||||
IsPrivate = isPrivate;
|
||||
@@ -25,11 +25,27 @@
|
||||
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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Models
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public string Name { get; }
|
||||
public Guid? WebSessionId { get; }
|
||||
|
||||
public bool IsGuest => WebSessionId.HasValue;
|
||||
|
||||
public User(string name)
|
||||
public static readonly ReadOnlyCollection<string> Adjectives = new(new[] {
|
||||
"Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly"
|
||||
});
|
||||
public static readonly ReadOnlyCollection<string> Subjects = new(new[] {
|
||||
"Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato"
|
||||
});
|
||||
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>
|
||||
/// Constructor for guest user.
|
||||
/// </summary>
|
||||
public User(string name, Guid webSessionId)
|
||||
public string Id { get; }
|
||||
public string DisplayName { get; }
|
||||
|
||||
public WhichLoginPlatform LoginPlatform { get; }
|
||||
|
||||
public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest;
|
||||
|
||||
public User(string id, string displayName, WhichLoginPlatform platform)
|
||||
{
|
||||
Name = name;
|
||||
WebSessionId = webSessionId;
|
||||
Id = id;
|
||||
DisplayName = displayName;
|
||||
LoginPlatform = platform;
|
||||
}
|
||||
|
||||
public ClaimsIdentity CreateMsalUserIdentity()
|
||||
public User(UserDocument document)
|
||||
{
|
||||
var claims = new List<Claim>()
|
||||
Id = document.Id;
|
||||
DisplayName = document.DisplayName;
|
||||
LoginPlatform = document.Platform;
|
||||
}
|
||||
|
||||
public ClaimsIdentity CreateClaimsIdentity()
|
||||
{
|
||||
if (LoginPlatform == WhichLoginPlatform.Guest)
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, Name),
|
||||
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||
};
|
||||
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
public ClaimsIdentity CreateGuestUserIdentity()
|
||||
{
|
||||
// TODO: Make this method static and factory-like.
|
||||
if (!WebSessionId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create guest identity without a session identifier.");
|
||||
var claims = new List<Claim>(4)
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, Id),
|
||||
new Claim(ClaimTypes.Name, DisplayName),
|
||||
new Claim(ClaimTypes.Role, "Guest"),
|
||||
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||
};
|
||||
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
var claims = new List<Claim>()
|
||||
else
|
||||
{
|
||||
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);
|
||||
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
|
||||
{
|
||||
Unknown,
|
||||
Microsoft,
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
public abstract class CouchDocument
|
||||
{
|
||||
[JsonProperty("_id")] public string Id { get; set; }
|
||||
[JsonProperty("_rev")] public string? RevisionId { get; set; }
|
||||
public WhichDocumentType DocumentType { get; }
|
||||
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 string Name { get; set; }
|
||||
public string Player1 { get; set; }
|
||||
public string? Player2 { get; set; }
|
||||
public string Player1Id { get; set; }
|
||||
public string? Player2Id { get; set; }
|
||||
public bool IsPrivate { get; set; }
|
||||
public IList<BoardStateDocument> History { get; set; }
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
public SessionDocument() : base(WhichDocumentType.Session)
|
||||
{
|
||||
Name = string.Empty;
|
||||
Player1 = string.Empty;
|
||||
Player2 = string.Empty;
|
||||
Player1Id = string.Empty;
|
||||
Player2Id = string.Empty;
|
||||
History = new List<BoardStateDocument>(0);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
: base(session.Name, WhichDocumentType.Session)
|
||||
{
|
||||
Name = session.Name;
|
||||
Player1 = session.Player1;
|
||||
Player2 = session.Player2;
|
||||
Player1Id = session.Player1.Id;
|
||||
Player2Id = session.Player2?.Id;
|
||||
IsPrivate = session.IsPrivate;
|
||||
History = new List<BoardStateDocument>(0);
|
||||
}
|
||||
@@ -35,14 +35,10 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
: base(sessionMetaData.Name, WhichDocumentType.Session)
|
||||
{
|
||||
Name = sessionMetaData.Name;
|
||||
Player1 = sessionMetaData.Player1;
|
||||
Player2 = sessionMetaData.Player2;
|
||||
Player1Id = sessionMetaData.Player1.Id;
|
||||
Player2Id = sessionMetaData.Player2?.Id;
|
||||
IsPrivate = sessionMetaData.IsPrivate;
|
||||
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 System;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
{
|
||||
public class UserDocument : CouchDocument
|
||||
{
|
||||
|
||||
public string Name { get; set; }
|
||||
public string DisplayName { 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>
|
||||
/// Constructor for JSON deserializing.
|
||||
/// </summary>
|
||||
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;
|
||||
WebSessionId = webSessionId;
|
||||
Platform = WebSessionId.HasValue
|
||||
? WhichLoginPlatform.Guest
|
||||
: WhichLoginPlatform.Microsoft;
|
||||
DisplayName = displayName;
|
||||
Platform = platform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
@@ -16,10 +20,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
Task<bool> CreateBoardState(Models.Session session);
|
||||
Task<bool> CreateSession(Models.SessionMetadata session);
|
||||
Task<bool> CreateUser(Models.User user);
|
||||
Task<IList<Models.SessionMetadata>> ReadSessionMetadatas();
|
||||
Task<Models.User?> ReadGuestUser(Guid webSessionId);
|
||||
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
|
||||
Task<Models.Session?> ReadSession(string name);
|
||||
Task<Models.Shogi?> ReadShogi(string name);
|
||||
Task<bool> UpdateSession(Models.SessionMetadata session);
|
||||
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
|
||||
Task<Models.User?> ReadUser(string userName);
|
||||
@@ -27,6 +29,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
|
||||
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 readonly HttpClient client;
|
||||
private readonly ILogger<GameboardRepository> logger;
|
||||
@@ -37,86 +48,123 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IList<Models.SessionMetadata>> ReadSessionMetadatas()
|
||||
public async Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas()
|
||||
{
|
||||
var selector = new Dictionary<string, object>(2)
|
||||
{
|
||||
[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 queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
|
||||
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var results = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent);
|
||||
if (results != null)
|
||||
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||
if (result != null)
|
||||
{
|
||||
return results
|
||||
.docs
|
||||
.Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
|
||||
.ToList();
|
||||
var groupedBySession = result.rows.GroupBy(row => row.id);
|
||||
var sessions = new List<Models.SessionMetadata>(result.total_rows / 3);
|
||||
foreach (var group in groupedBySession)
|
||||
{
|
||||
/**
|
||||
* 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 Collection<Models.SessionMetadata>(sessions);
|
||||
}
|
||||
|
||||
return new List<Models.SessionMetadata>(0);
|
||||
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
|
||||
}
|
||||
|
||||
public async Task<Models.Session?> ReadSession(string name)
|
||||
{
|
||||
var readShogiTask = ReadShogi(name);
|
||||
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)
|
||||
var queryParams = new QueryBuilder
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 couchModel = JsonConvert.DeserializeObject<SessionDocument>(responseContent);
|
||||
return couchModel.ToDomainModel();
|
||||
}
|
||||
|
||||
public async Task<Models.Shogi?> ReadShogi(string name)
|
||||
{
|
||||
var selector = new Dictionary<string, object>(2)
|
||||
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||
if (result != null && result.rows.Length > 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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -149,7 +197,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
|
||||
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 response = await client.PutAsync(couchModel.Id, content);
|
||||
return response.IsSuccessStatusCode;
|
||||
@@ -205,66 +262,31 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
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);
|
||||
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<UserDocument>(responseContent);
|
||||
{ "include_docs", "true" },
|
||||
{ "key", JsonConvert.SerializeObject(id) },
|
||||
}.ToQueryString();
|
||||
var response = await client.GetAsync($"{View_User}{queryParams}");
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<CouchViewResult<UserDocument>>(responseContent);
|
||||
if (result != null && result.rows.Length > 0)
|
||||
{
|
||||
return new Models.User(result.rows[0].doc);
|
||||
}
|
||||
|
||||
return new Models.User(user.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 response = await client.PostAsync(string.Empty, content);
|
||||
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 Gameboard.ShogiUI.Sockets.Controllers;
|
||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||
using Gameboard.ShogiUI.Sockets.Managers;
|
||||
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
|
||||
@@ -34,7 +33,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
||||
private readonly IGameboardManager gameboardManager;
|
||||
private readonly ISocketTokenCache tokenManager;
|
||||
private readonly IJoinByCodeHandler joinByCodeHandler;
|
||||
private readonly IJoinGameHandler joinGameHandler;
|
||||
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
|
||||
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
|
||||
|
||||
@@ -45,7 +43,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
||||
IGameboardManager gameboardManager,
|
||||
ISocketTokenCache tokenManager,
|
||||
IJoinByCodeHandler joinByCodeHandler,
|
||||
IJoinGameHandler joinGameHandler,
|
||||
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
|
||||
IValidator<JoinGameRequest> joinGameRequestValidator
|
||||
) : base()
|
||||
@@ -56,85 +53,68 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
||||
this.gameboardManager = gameboardManager;
|
||||
this.tokenManager = tokenManager;
|
||||
this.joinByCodeHandler = joinByCodeHandler;
|
||||
this.joinGameHandler = joinGameHandler;
|
||||
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
|
||||
this.joinGameRequestValidator = joinGameRequestValidator;
|
||||
}
|
||||
|
||||
public async Task HandleSocketRequest(HttpContext context)
|
||||
{
|
||||
string? userName = null;
|
||||
var user = await gameboardManager.ReadUser(context.User);
|
||||
if (user?.WebSessionId != null)
|
||||
if (!context.Request.Query.Keys.Contains("token"))
|
||||
{
|
||||
// Guest account
|
||||
userName = tokenManager.GetUsername(user.WebSessionId.Value);
|
||||
}
|
||||
else if (context.Request.Query.Keys.Contains("token"))
|
||||
{
|
||||
// Microsoft account
|
||||
var token = Guid.Parse(context.Request.Query["token"][0]);
|
||||
userName = tokenManager.GetUsername(token);
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
var token = Guid.Parse(context.Request.Query["token"][0]);
|
||||
var userName = tokenManager.GetUsername(token);
|
||||
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
communicationManager.SubscribeToBroadcast(socket, userName);
|
||||
while (socket.State == WebSocketState.Open)
|
||||
communicationManager.SubscribeToBroadcast(socket, userName);
|
||||
while (socket.State == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
var message = await socket.ReceiveTextAsync();
|
||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||
logger.LogInformation("Request \n{0}\n", message);
|
||||
var request = JsonConvert.DeserializeObject<Request>(message);
|
||||
if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action))
|
||||
{
|
||||
var message = await socket.ReceiveTextAsync();
|
||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||
logger.LogInformation("Request \n{0}\n", message);
|
||||
var request = JsonConvert.DeserializeObject<Request>(message);
|
||||
if (!Enum.IsDefined(typeof(ClientAction), request.Action))
|
||||
{
|
||||
await socket.SendTextAsync("Error: Action not recognized.");
|
||||
continue;
|
||||
}
|
||||
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:
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
|
||||
if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
|
||||
{
|
||||
await joinByCodeHandler.Handle(req, userName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
await socket.SendTextAsync("Error: Action not recognized.");
|
||||
continue;
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
switch (request.Action)
|
||||
{
|
||||
logger.LogError(ex.Message);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
|
||||
logger.LogInformation("Probably tried writing to a closed socket.");
|
||||
logger.LogError(ex.Message);
|
||||
case ClientAction.JoinByCode:
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
|
||||
if (req != null && await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
|
||||
{
|
||||
await joinByCodeHandler.Handle(req, userName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogError(ex.Message);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
|
||||
logger.LogInformation("Probably tried writing to a closed socket.");
|
||||
logger.LogError(ex.Message);
|
||||
}
|
||||
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,11 +124,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
||||
if (!results.IsValid)
|
||||
{
|
||||
var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage));
|
||||
var message = JsonConvert.SerializeObject(new Response
|
||||
{
|
||||
Error = errors
|
||||
});
|
||||
await socket.SendTextAsync(message);
|
||||
await socket.SendTextAsync(errors);
|
||||
}
|
||||
return results.IsValid;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,5 @@ namespace Gameboard.ShogiUI.Sockets.Services.Utility
|
||||
public class Response : IResponse
|
||||
{
|
||||
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);
|
||||
if (user == null)
|
||||
{
|
||||
var newUser = new Models.User(nameClaim.Value);
|
||||
var newUser = Models.User.CreateMsalUser(nameClaim.Value);
|
||||
var success = await gameboardRepository.CreateUser(newUser);
|
||||
if (success) user = newUser;
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
return new ClaimsPrincipal(user.CreateMsalUserIdentity());
|
||||
return new ClaimsPrincipal(user.CreateClaimsIdentity());
|
||||
}
|
||||
}
|
||||
return principal;
|
||||
|
||||
@@ -13,7 +13,9 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Identity.Client;
|
||||
using Microsoft.Identity.Web;
|
||||
using Newtonsoft.Json;
|
||||
@@ -41,7 +43,6 @@ namespace Gameboard.ShogiUI.Sockets
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
|
||||
services.AddSingleton<IJoinGameHandler, JoinGameHandler>();
|
||||
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
||||
services.AddSingleton<ISocketTokenCache, SocketTokenCache>();
|
||||
services.AddSingleton<IGameboardManager, GameboardManager>();
|
||||
@@ -109,6 +110,9 @@ namespace 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.
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Default": "Warning",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
"Microsoft.Hosting.Lifetime": "Error"
|
||||
}
|
||||
},
|
||||
"AzureAd": {
|
||||
|
||||
Reference in New Issue
Block a user