diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs index 41f93ec..90fc455 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs @@ -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; } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs similarity index 75% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs index c15dae4..d1e3cf7 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs @@ -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; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs new file mode 100644 index 0000000..cc972bc --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs @@ -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 PlayerHasJoinedSessions { get; set; } + public Collection AllOtherSessions { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs index 48d1b34..22fac9c 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs @@ -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; } + /// + /// The player who joined the game. + /// public string PlayerName { get; set; } public JoinGameResponse() { Action = ClientAction.JoinGame.ToString(); - Error = ""; GameName = ""; PlayerName = ""; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs index 0853998..3faf4b5 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs @@ -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 MoveHistory { get; set; } + public string Action { get; } + public string GameName { get; set; } + /// + /// The player that made the move. + /// + public string PlayerName { get; set; } public MoveResponse() { diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 718573f..e5ebad0 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -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 PostSession([FromBody] PostSession request) @@ -125,8 +130,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [HttpPost] public async Task PostSession([FromBody] PostSession request) { - var user = await gameboardManager.ReadUser(User); - var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!.Name); + var 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 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 GetSessions() + public async Task 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(sessionsJoinedByUser), + AllOtherSessions = new Collection(sessionsNotJoinedByUser) + }; + } + + [HttpPut("{gameName}")] + public async Task 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 ReadUserOrThrow() + { + var user = await gameboardManager.ReadUser(User); + if (user == null) + { + throw new UnauthorizedAccessException("Unknown user claims."); + } + return user; } } } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 0b9e941..5b99eb3 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -54,24 +54,21 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [HttpGet("Token")] public async Task 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 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)); } } } diff --git a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs index 30517ad..ca6dc5c 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs @@ -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..]; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs deleted file mode 100644 index 52415ee..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs index e4c4aa0..5cc9f04 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs @@ -9,9 +9,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers { public interface IGameboardManager { - Task IsPlayer1(string sessionName, string playerName); - Task AssignPlayer2ToSession(string sessionName, string userName); + Task AssignPlayer2ToSession(string sessionName, User user); Task ReadUser(ClaimsPrincipal user); + Task CreateUser(ClaimsPrincipal user); } public class GameboardManager : IGameboardManager @@ -23,14 +23,25 @@ namespace Gameboard.ShogiUI.Sockets.Managers this.repository = repository; } - public Task ReadUser(ClaimsPrincipal user) + public Task 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 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(null); } - public async Task IsPlayer1(string sessionName, string playerName) - { - //var session = await repository.GetGame(sessionName); - //return session?.Player1 == playerName; - return true; - } public async Task CreateJoinCode(string sessionName, string playerName) { @@ -55,19 +60,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers return string.Empty; } - public async Task AssignPlayer2ToSession(string sessionName, string userName) + public async Task 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; } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 720381d..10fa790 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -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) diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index 67240e0..cd5c5dd 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -10,13 +10,14 @@ namespace Gameboard.ShogiUI.Sockets.Models // TODO: Separate subscriptions to the Session from the Session. [JsonIgnore] public ConcurrentDictionary 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(); @@ -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 }; } } diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs index d46b8af..ac35d6f 100644 --- a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -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); + } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs index acfd4a9..60f5b2b 100644 --- a/Gameboard.ShogiUI.Sockets/Models/User.cs +++ b/Gameboard.ShogiUI.Sockets/Models/User.cs @@ -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 Adjectives = new(new[] { + "Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly" + }); + public static readonly ReadOnlyCollection 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); } - /// - /// Constructor for guest user. - /// - 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() + 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(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() + 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(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); + } } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs index 5923404..5d2378e 100644 --- a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs +++ b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs @@ -2,6 +2,7 @@ { public enum WhichLoginPlatform { + Unknown, Microsoft, Guest } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs index 08de6a5..ecd0450 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs @@ -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; diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs index 4549de0..29d19c3 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs @@ -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; } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs new file mode 100644 index 0000000..ca98d2e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs @@ -0,0 +1,28 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class CouchViewResult where T : class + { + public int total_rows; + public int offset; + public CouchViewResultRow[] rows; + + public CouchViewResult() + { + rows = Array.Empty>(); + } + } + + public class CouchViewResultRow + { + public string id; + public T doc; + + public CouchViewResultRow() + { + id = string.Empty; + doc = default!; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs index 59e8bf8..6db129e 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs @@ -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 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(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(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(0); } - - public Models.Session ToDomainModel(Models.Shogi shogi) => new(Name, IsPrivate, shogi, Player1, Player2); - - public Models.SessionMetadata ToDomainModel() => new(Name, IsPrivate, Player1, Player2); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs index c41b60b..940a8bd 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs @@ -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; } - /// - /// The browser session ID saved via Set-Cookie headers. - /// Only used with guest accounts. - /// - public Guid? WebSessionId { get; set; } /// /// Constructor for JSON deserializing. /// 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; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 89ee28c..3489693 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -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 CreateBoardState(Models.Session session); Task CreateSession(Models.SessionMetadata session); Task CreateUser(Models.User user); - Task> ReadSessionMetadatas(); - Task ReadGuestUser(Guid webSessionId); + Task> ReadSessionMetadatas(); Task ReadSession(string name); - Task ReadShogi(string name); Task UpdateSession(Models.SessionMetadata session); Task ReadSessionMetaData(string name); Task ReadUser(string userName); @@ -27,6 +29,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public class GameboardRepository : IGameboardRepository { + /// + /// Returns session, board state, and user documents, grouped by session. + /// + private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate"; + /// + /// Returns session and user documents, grouped by session. + /// + 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 logger; @@ -37,86 +48,123 @@ namespace Gameboard.ShogiUI.Sockets.Repositories this.logger = logger; } - public async Task> ReadSessionMetadatas() + public async Task> ReadSessionMetadatas() { - var selector = new Dictionary(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>(responseContent); - if (results != null) + var result = JsonConvert.DeserializeObject>(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(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(); + var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject(); + var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject(); + 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(sessions); } - - return new List(0); + return new Collection(Array.Empty()); } public async Task ReadSession(string name) { - var readShogiTask = ReadShogi(name); - var response = await client.GetAsync(name); - var responseContent = await response.Content.ReadAsStringAsync(); - var couchModel = JsonConvert.DeserializeObject(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>(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(); + var player1Doc = group[1].doc.ToObject(); + var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); + var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() + ? group[2].doc.ToObject() + : 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()) + .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 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(responseContent); - return couchModel.ToDomainModel(); - } - - public async Task ReadShogi(string name) - { - var selector = new Dictionary(2) + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null && result.rows.Length > 2) { - [nameof(BoardStateDocument.DocumentType)] = WhichDocumentType.BoardState, - [nameof(BoardStateDocument.Name)] = name - }; - var sort = new Dictionary(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(); + var player1Doc = group[1].doc.ToObject(); + var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); + var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() + ? group[2].doc.ToObject() + : 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>(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; } /// @@ -149,7 +197,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task 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(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 ReadUser(string userName) + public async Task 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(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>(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 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 ReadGuestUser(Guid webSessionId) - { - var selector = new Dictionary(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>(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; - } } } diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs index 30fff7f..0418950 100644 --- a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs +++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs @@ -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 joinByCodeRequestValidator; private readonly IValidator joinGameRequestValidator; @@ -45,7 +43,6 @@ namespace Gameboard.ShogiUI.Sockets.Services IGameboardManager gameboardManager, ISocketTokenCache tokenManager, IJoinByCodeHandler joinByCodeHandler, - IJoinGameHandler joinGameHandler, IValidator joinByCodeRequestValidator, IValidator 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(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(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(message); - if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req)) - { - await joinGameHandler.Handle(req, userName); - } - break; - } - case ClientAction.JoinByCode: - { - var req = JsonConvert.DeserializeObject(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(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; } diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs index f33af9f..067d374 100644 --- a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs +++ b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs @@ -5,6 +5,5 @@ namespace Gameboard.ShogiUI.Sockets.Services.Utility public class Response : IResponse { public string Action { get; set; } - public string Error { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs index 1cbc725..a04a25b 100644 --- a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs +++ b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs @@ -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; diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 92f88e5..5650206 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -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(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -109,6 +110,9 @@ namespace Gameboard.ShogiUI.Sockets document.Info.Title = "Gameboard.ShogiUI.Sockets"; }; }); + + // Remove default HttpClient logging. + services.RemoveAll(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 060e229..7abd699 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -7,9 +7,9 @@ }, "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Warning", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Error" } }, "AzureAd": {