From 1d0beaf69fefc456a79a294a1883d50a76fa0f87 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 19 Jan 2023 16:20:41 -0600 Subject: [PATCH] reintroduce microsoft login. upgrade a bunch of stuff. --- Shogi.Api/Controllers/UserController.cs | 32 +-- Shogi.Api/Program.cs | 7 +- Shogi.Api/Repositories/UserRepository.cs | 63 ++--- Shogi.Api/Shogi.Api.csproj | 13 +- Shogi.Contracts/Shogi.Contracts.csproj | 2 +- Shogi.Domain/Shogi.Domain.csproj | 2 +- Shogi.UI/App.razor | 4 +- Shogi.UI/Pages/Authentication.razor | 5 +- Shogi.UI/Pages/Home/Account/AccountManager.cs | 241 +++++++++--------- Shogi.UI/Pages/Home/Account/AccountState.cs | 42 +-- Shogi.UI/Pages/Home/Account/User.cs | 16 +- Shogi.UI/Pages/Home/Api/ShogiApi.cs | 137 +++++----- Shogi.UI/Pages/Home/GameBoard.razor | 185 -------------- .../Pages/Home/GameBoard/EmptyGameBoard.razor | 11 + Shogi.UI/Pages/Home/GameBoard/GameBoard.razor | 45 ++++ .../GameBoard/GameBoardPresentation.razor | 116 +++++++++ .../GameboardPresentation.razor.css} | 0 .../Home/GameBoard/SeatedGameBoard.razor | 71 ++++++ .../Home/GameBoard/SpectatorGameBoard.razor | 8 + Shogi.UI/Pages/Home/GameBrowser.razor | 42 ++- Shogi.UI/Pages/Home/Home.razor | 9 +- Shogi.UI/Pages/Home/PageHeader.razor | 2 +- Shogi.UI/Program.cs | 7 +- Shogi.UI/Shared/ShogiSocket.cs | 1 + Shogi.UI/Shogi.UI.csproj | 12 +- Shogi.UI/_Imports.razor | 1 + Tests/AcceptanceTests/AcceptanceTests.csproj | 6 +- Tests/UnitTests/UnitTests.csproj | 2 +- global.json | 2 +- 29 files changed, 601 insertions(+), 483 deletions(-) delete mode 100644 Shogi.UI/Pages/Home/GameBoard.razor create mode 100644 Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor create mode 100644 Shogi.UI/Pages/Home/GameBoard/GameBoard.razor create mode 100644 Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor rename Shogi.UI/Pages/Home/{GameBoard.razor.css => GameBoard/GameboardPresentation.razor.css} (100%) create mode 100644 Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor create mode 100644 Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor diff --git a/Shogi.Api/Controllers/UserController.cs b/Shogi.Api/Controllers/UserController.cs index 10292ad..64140c2 100644 --- a/Shogi.Api/Controllers/UserController.cs +++ b/Shogi.Api/Controllers/UserController.cs @@ -38,23 +38,8 @@ public class UserController : ControllerBase }; } - [HttpPut("GuestLogout")] - public async Task GuestLogout() - { - var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - - var userId = User?.GetGuestUserId(); - if (!string.IsNullOrEmpty(userId)) - { - connectionManager.Unsubscribe(userId); - } - - await signoutTask; - return Ok(); - } - [HttpGet("Token")] - public ActionResult GetToken() + public ActionResult GetWebSocketToken() { var userId = User.GetShogiUserId(); var displayName = User.DisplayName(); @@ -83,4 +68,19 @@ public class UserController : ControllerBase } return Ok(); } + + [HttpPut("GuestLogout")] + public async Task GuestLogout() + { + var signOutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + var userId = User?.GetGuestUserId(); + if (!string.IsNullOrEmpty(userId)) + { + connectionManager.Unsubscribe(userId); + } + + await signOutTask; + return Ok(); + } } diff --git a/Shogi.Api/Program.cs b/Shogi.Api/Program.cs index 15679ce..a3194d3 100644 --- a/Shogi.Api/Program.cs +++ b/Shogi.Api/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.HttpLogging; +using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; using Shogi.Api.Managers; using Shogi.Api.Repositories; @@ -119,9 +120,9 @@ namespace Shogi.Api static void AddJwtAuth(WebApplicationBuilder builder) { - //builder.Services - // .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - //.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + builder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); } static void AddCookieAuth(WebApplicationBuilder builder) diff --git a/Shogi.Api/Repositories/UserRepository.cs b/Shogi.Api/Repositories/UserRepository.cs index 1980739..9f54dbf 100644 --- a/Shogi.Api/Repositories/UserRepository.cs +++ b/Shogi.Api/Repositories/UserRepository.cs @@ -7,41 +7,46 @@ namespace Shogi.Api.Repositories; public class UserRepository : IUserRepository { - private readonly string connectionString; + private readonly string connectionString; - public UserRepository(IConfiguration configuration) - { - connectionString = configuration.GetConnectionString("ShogiDatabase"); - } + public UserRepository(IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("ShogiDatabase"); + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("Connection string for database is empty."); + } + this.connectionString = connectionString; + } - public async Task CreateUser(User user) - { - using var connection = new SqlConnection(connectionString); - await connection.ExecuteAsync( - "user.CreateUser", - new - { - Name = user.Id, - DisplayName = user.DisplayName, - Platform = user.LoginPlatform.ToString() - }, - commandType: CommandType.StoredProcedure); - } + public async Task CreateUser(User user) + { + using var connection = new SqlConnection(connectionString); + await connection.ExecuteAsync( + "user.CreateUser", + new + { + Name = user.Id, + DisplayName = user.DisplayName, + Platform = user.LoginPlatform.ToString() + }, + commandType: CommandType.StoredProcedure); + } - public async Task ReadUser(string id) - { - using var connection = new SqlConnection(connectionString); - var results = await connection.QueryAsync( - "user.ReadUser", - new { Name = id }, - commandType: CommandType.StoredProcedure); + public async Task ReadUser(string id) + { + using var connection = new SqlConnection(connectionString); + var results = await connection.QueryAsync( + "user.ReadUser", + new { Name = id }, + commandType: CommandType.StoredProcedure); - return results.FirstOrDefault(); - } + return results.FirstOrDefault(); + } } public interface IUserRepository { - Task CreateUser(User user); - Task ReadUser(string id); + Task CreateUser(User user); + Task ReadUser(string id); } \ No newline at end of file diff --git a/Shogi.Api/Shogi.Api.csproj b/Shogi.Api/Shogi.Api.csproj index f06f761..acf4fa9 100644 --- a/Shogi.Api/Shogi.Api.csproj +++ b/Shogi.Api/Shogi.Api.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 true 5 enable @@ -24,12 +24,13 @@ - + - - - - + + + + + diff --git a/Shogi.Contracts/Shogi.Contracts.csproj b/Shogi.Contracts/Shogi.Contracts.csproj index 8c5839a..b80b4ad 100644 --- a/Shogi.Contracts/Shogi.Contracts.csproj +++ b/Shogi.Contracts/Shogi.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 true 5 enable diff --git a/Shogi.Domain/Shogi.Domain.csproj b/Shogi.Domain/Shogi.Domain.csproj index 0c0a0ef..e21c6b5 100644 --- a/Shogi.Domain/Shogi.Domain.csproj +++ b/Shogi.Domain/Shogi.Domain.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 disable enable diff --git a/Shogi.UI/App.razor b/Shogi.UI/App.razor index 05d13a9..38fe056 100644 --- a/Shogi.UI/App.razor +++ b/Shogi.UI/App.razor @@ -3,7 +3,7 @@ - + @* Authorizing!! @@ -17,7 +17,7 @@

You are not authorized to access this resource.

} -
+
*@
diff --git a/Shogi.UI/Pages/Authentication.razor b/Shogi.UI/Pages/Authentication.razor index d1b6cf9..94ca00f 100644 --- a/Shogi.UI/Pages/Authentication.razor +++ b/Shogi.UI/Pages/Authentication.razor @@ -1,7 +1,10 @@ @page "/authentication/{action}" @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@**@ + @code{ [Parameter] public string? Action { get; set; } + // https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/RemoteAuthenticationActions.cs + // https://github.com/dotnet/aspnetcore/blob/7c810658463f35c39c54d5fb8a8dbbfd463bf747/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.cs } + diff --git a/Shogi.UI/Pages/Home/Account/AccountManager.cs b/Shogi.UI/Pages/Home/Account/AccountManager.cs index 156ba35..569e52a 100644 --- a/Shogi.UI/Pages/Home/Account/AccountManager.cs +++ b/Shogi.UI/Pages/Home/Account/AccountManager.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Shogi.UI.Pages.Home.Api; using Shogi.UI.Shared; @@ -7,132 +8,144 @@ namespace Shogi.UI.Pages.Home.Account; public class AccountManager { - private readonly AccountState accountState; - private readonly IShogiApi shogiApi; - private readonly IConfiguration configuration; - private readonly ILocalStorage localStorage; - private readonly AuthenticationStateProvider authState; - private readonly NavigationManager navigation; - private readonly ShogiSocket shogiSocket; + private readonly AccountState accountState; + private readonly IShogiApi shogiApi; + private readonly IConfiguration configuration; + private readonly ILocalStorage localStorage; + private readonly AuthenticationStateProvider authState; + private readonly NavigationManager navigation; + private readonly ShogiSocket shogiSocket; - public AccountManager( - AccountState accountState, - IShogiApi unauthenticatedClient, - IConfiguration configuration, - AuthenticationStateProvider authState, - ILocalStorage localStorage, - NavigationManager navigation, - ShogiSocket shogiSocket) - { - this.accountState = accountState; - this.shogiApi = unauthenticatedClient; - this.configuration = configuration; - this.authState = authState; - this.localStorage = localStorage; - this.navigation = navigation; - this.shogiSocket = shogiSocket; - } - - private User? User { get => accountState.User; set => accountState.User = value; } - - public async Task LoginWithGuestAccount() - { - var response = await shogiApi.GetToken(); - if (response != null) + public AccountManager( + AccountState accountState, + IShogiApi unauthenticatedClient, + IConfiguration configuration, + AuthenticationStateProvider authState, + ILocalStorage localStorage, + NavigationManager navigation, + ShogiSocket shogiSocket) { - User = new User - { - DisplayName = response.DisplayName, - Id = response.UserId, - OneTimeSocketToken = response.OneTimeToken, - WhichAccountPlatform = WhichAccountPlatform.Guest - }; - await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); - await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest); - } - } - - public async Task LoginWithMicrosoftAccount() - { - var state = await authState.GetAuthenticationStateAsync(); - - if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated != true) - { - navigation.NavigateTo("authentication/login"); - return; + this.accountState = accountState; + this.shogiApi = unauthenticatedClient; + this.configuration = configuration; + this.authState = authState; + this.localStorage = localStorage; + this.navigation = navigation; + this.shogiSocket = shogiSocket; } - var response = await shogiApi.GetToken(); - if (response != null) - { - User = new User - { - DisplayName = response.DisplayName, - Id = response.UserId, - OneTimeSocketToken = response.OneTimeToken, - WhichAccountPlatform = WhichAccountPlatform.Microsoft, - }; + private User? MyUser { get => accountState.User; set => accountState.User = value; } - await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); - await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft); - } - } - - /// - /// Try to log in with the account used from the previous browser session. - /// - /// - public async Task TryLoginSilentAsync() - { - var platform = await localStorage.GetAccountPlatform(); - if (platform == WhichAccountPlatform.Guest) + public async Task LoginWithGuestAccount() { - var response = await shogiApi.GetToken(); - if (response != null) - { - User = new User + var response = await shogiApi.GetToken(); + if (response != null) { - DisplayName = response.DisplayName, - Id = response.UserId, - OneTimeSocketToken = response.OneTimeToken, - WhichAccountPlatform = WhichAccountPlatform.Guest - }; - } + MyUser = new User + { + DisplayName = response.DisplayName, + Id = response.UserId, + OneTimeSocketToken = response.OneTimeToken, + WhichAccountPlatform = WhichAccountPlatform.Guest + }; + await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString()); + await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest); + } } - else if (platform == WhichAccountPlatform.Microsoft) + + public async Task LoginWithMicrosoftAccount() { - Console.WriteLine("Login Microsoft"); - throw new NotImplementedException(); - //var state = await authState.GetAuthenticationStateAsync(); - //if (state.User?.Identity?.Name != null) - //{ - // var id = state.User.Identity; - // User = new User - // { - // DisplayName = id.Name, - // Id = id.Name - // }; - // var token = await shogiApi.GetToken(); - // if (token.HasValue) - // { - // User.OneTimeSocketToken = token.Value; - // } - //} - // TODO: If this fails then platform saved to localStorage should get cleared + var state = await authState.GetAuthenticationStateAsync(); + + if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated == false) + { + // Set the login platform so that we know to log in with microsoft after being redirected away from the UI. + await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft); + navigation.NavigateToLogin("authentication/login"); + return; + } + + //var response = await shogiApi.GetToken(); + //if (response != null) + //{ + // MyUser = new User + // { + // DisplayName = response.DisplayName, + // Id = response.UserId, + // OneTimeSocketToken = response.OneTimeToken, + // WhichAccountPlatform = WhichAccountPlatform.Microsoft, + // }; + + // await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString()); + // await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft); + //} } - if (User != null) + /// + /// Try to log in with the account used from the previous browser session. + /// + /// + public async Task TryLoginSilentAsync() { - await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); - return true; + var platform = await localStorage.GetAccountPlatform(); + Console.WriteLine($"Try Login Silent - {platform}"); + if (platform == WhichAccountPlatform.Guest) + { + var response = await shogiApi.GetToken(); + if (response != null) + { + MyUser = new User + { + DisplayName = response.DisplayName, + Id = response.UserId, + OneTimeSocketToken = response.OneTimeToken, + WhichAccountPlatform = WhichAccountPlatform.Guest + }; + } + } + else if (platform == WhichAccountPlatform.Microsoft) + { + var state = await authState.GetAuthenticationStateAsync(); + if (state.User?.Identity?.Name != null) + { + var response = await shogiApi.GetToken(); + if (response == null) + { + // Login failed, so reset local storage to avoid putting the user in a broken state. + await localStorage.DeleteAccountPlatform(); + return false; + } + var id = state.User.Identity; + MyUser = new User + { + DisplayName = id.Name, + Id = id.Name, + OneTimeSocketToken = response.OneTimeToken + }; + } + } + + if (MyUser != null) + { + await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString()); + return true; + } + + return false; } - return false; - } - - public async Task LogoutAsync() - { - await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform()); - User = null; - } + public async Task LogoutAsync() + { + MyUser = null; + var platform = await localStorage.GetAccountPlatform(); + await localStorage.DeleteAccountPlatform(); + + if (platform == WhichAccountPlatform.Guest) + { + await shogiApi.GuestLogout(); + } else if (platform == WhichAccountPlatform.Microsoft) + { + navigation.NavigateToLogout("authentication/logout"); + } + } } diff --git a/Shogi.UI/Pages/Home/Account/AccountState.cs b/Shogi.UI/Pages/Home/Account/AccountState.cs index 6abcf85..c5b7e85 100644 --- a/Shogi.UI/Pages/Home/Account/AccountState.cs +++ b/Shogi.UI/Pages/Home/Account/AccountState.cs @@ -2,27 +2,27 @@ public class AccountState { - public event EventHandler? LoginChangedEvent; + public event EventHandler? LoginChangedEvent; - private User? user; - public User? User - { - get => user; - set - { - if (user != value) - { - user = value; - EmitLoginChangedEvent(); - } - } - } + private User? user; + public User? User + { + get => user; + set + { + if (user != value) + { + user = value; + EmitLoginChangedEvent(); + } + } + } - private void EmitLoginChangedEvent() - { - LoginChangedEvent?.Invoke(this, new LoginEventArgs - { - User = User - }); - } + private void EmitLoginChangedEvent() + { + LoginChangedEvent?.Invoke(this, new LoginEventArgs + { + User = User + }); + } } diff --git a/Shogi.UI/Pages/Home/Account/User.cs b/Shogi.UI/Pages/Home/Account/User.cs index d91626d..caada77 100644 --- a/Shogi.UI/Pages/Home/Account/User.cs +++ b/Shogi.UI/Pages/Home/Account/User.cs @@ -1,12 +1,10 @@ namespace Shogi.UI.Pages.Home.Account { - public class User - { - public string Id { get; set; } - public string DisplayName { get; set; } - - public WhichAccountPlatform WhichAccountPlatform { get; set; } - - public Guid OneTimeSocketToken { get; set; } - } + public readonly record struct User( + string Id, + string DisplayName, + WhichAccountPlatform WhichAccountPlatform, + Guid OneTimeSocketToken) + { + } } diff --git a/Shogi.UI/Pages/Home/Api/ShogiApi.cs b/Shogi.UI/Pages/Home/Api/ShogiApi.cs index 5f025c2..754118d 100644 --- a/Shogi.UI/Pages/Home/Api/ShogiApi.cs +++ b/Shogi.UI/Pages/Home/Api/ShogiApi.cs @@ -7,74 +7,75 @@ using System.Text.Json; namespace Shogi.UI.Pages.Home.Api { - public class ShogiApi : IShogiApi - { - public const string GuestClientName = "Guest"; - public const string MsalClientName = "Msal"; - public const string AnonymouseClientName = "Anonymous"; - - private readonly JsonSerializerOptions serializerOptions; - private readonly IHttpClientFactory clientFactory; - private readonly AccountState accountState; - - public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState) + public class ShogiApi : IShogiApi { - serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.clientFactory = clientFactory; - this.accountState = accountState; + public const string GuestClientName = "Guest"; + public const string MsalClientName = "Msal"; + //public const string AnonymousClientName = "Anonymous"; + + private readonly JsonSerializerOptions serializerOptions; + private readonly IHttpClientFactory clientFactory; + private readonly AccountState accountState; + + public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState) + { + serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + this.clientFactory = clientFactory; + this.accountState = accountState; + } + + private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch + { + WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName), + WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName), + _ => clientFactory.CreateClient(GuestClientName) + }; + + public async Task GuestLogout() + { + var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null); + response.EnsureSuccessStatusCode(); + } + + public async Task GetSession(string name) + { + var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative)); + if (response.IsSuccessStatusCode) + { + return (await response.Content.ReadFromJsonAsync(serializerOptions))?.Session; + } + return null; + } + + public async Task GetSessionsPlayerCount() + { + var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative)); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(serializerOptions); + } + return null; + } + + public async Task GetToken() + { + Console.WriteLine($"GetToken() - {accountState.User?.WhichAccountPlatform}"); + var response = await HttpClient.GetFromJsonAsync(new Uri("User/Token", UriKind.Relative), serializerOptions); + return response; + } + + public async Task Move(string sessionName, MovePieceCommand command) + { + await this.HttpClient.PatchAsync($"Sessions/{sessionName}/Move", JsonContent.Create(command)); + } + + public async Task PostSession(string name, bool isPrivate) + { + var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand + { + Name = name, + }); + return response.StatusCode; + } } - - private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch - { - WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName), - WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName), - _ => clientFactory.CreateClient(AnonymouseClientName) - }; - - public async Task GuestLogout() - { - var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null); - response.EnsureSuccessStatusCode(); - } - - public async Task GetSession(string name) - { - var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative)); - if (response.IsSuccessStatusCode) - { - return (await response.Content.ReadFromJsonAsync(serializerOptions))?.Session; - } - return null; - } - - public async Task GetSessionsPlayerCount() - { - var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative)); - if (response.IsSuccessStatusCode) - { - return await response.Content.ReadFromJsonAsync(serializerOptions); - } - return null; - } - - public async Task GetToken() - { - var response = await HttpClient.GetFromJsonAsync(new Uri("User/Token", UriKind.Relative), serializerOptions); - return response; - } - - public async Task Move(string sessionName, MovePieceCommand command) - { - await this.HttpClient.PatchAsync($"Sessions/{sessionName}/Move", JsonContent.Create(command)); - } - - public async Task PostSession(string name, bool isPrivate) - { - var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand - { - Name = name, - }); - return response.StatusCode; - } - } } diff --git a/Shogi.UI/Pages/Home/GameBoard.razor b/Shogi.UI/Pages/Home/GameBoard.razor deleted file mode 100644 index adb5552..0000000 --- a/Shogi.UI/Pages/Home/GameBoard.razor +++ /dev/null @@ -1,185 +0,0 @@ -@using Shogi.Contracts.Api -@using Shogi.Contracts.Types; -@using System.Text.RegularExpressions; -@inject IShogiApi ShogiApi -@inject AccountState Account; -@inject PromotePrompt PromotePrompt; - -
- -
- @for (var rank = 1; rank < 10; rank++) - { - foreach (var file in Files) - { - var position = $"{file}{rank}"; - var piece = session?.BoardState.Board[position]; -
- -
- } - } -
- 9 - 8 - 7 - 6 - 5 - 4 - 3 - 2 - 1 -
-
- A - B - C - D - E - F - G - H - I -
- -
-

Do you wish to promote?

-
- - - -
-
-
- - @if (session != null) - { - - } -
- -@code { - static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" }; - - [Parameter] - public string? SessionName { get; set; } - - WhichPlayer Perspective => Account.User?.Id == session?.Player1 - ? WhichPlayer.Player1 - : WhichPlayer.Player2; - Session? session; - IReadOnlyCollection OpponentHand - { - get - { - if (this.session == null) return Array.Empty(); - - return Perspective == WhichPlayer.Player1 - ? this.session.BoardState.Player1Hand - : this.session.BoardState.Player2Hand; - } - } - IReadOnlyCollection UserHand - { - get - { - if (this.session == null) return Array.Empty(); - - return Perspective == WhichPlayer.Player1 - ? this.session.BoardState.Player1Hand - : this.session.BoardState.Player2Hand; - } - } - bool IsMyTurn => session?.BoardState.WhoseTurn == Perspective; - - string? selectedPosition; - WhichPiece? selectedPiece; - - protected override async Task OnParametersSetAsync() - { - if (!string.IsNullOrWhiteSpace(SessionName)) - { - this.session = await ShogiApi.GetSession(SessionName); - } - } - - bool ShouldPromptForPromotion(string position) - { - if (Perspective == WhichPlayer.Player1 && Regex.IsMatch(position, ".[7-9]")) - { - return true; - } - if (Perspective == WhichPlayer.Player2 && Regex.IsMatch(position, ".[1-3]")) - { - return true; - } - return false; - } - - async void OnClickTile(Piece? piece, string position) - { - if (SessionName == null || !IsMyTurn) return; - - if (selectedPosition == null || piece?.Owner == Perspective) - { - // Select a position. - selectedPosition = position; - return; - } - if (selectedPosition == position) - { - // Deselect the selected position. - selectedPosition = null; - return; - } - if (piece == null) - { - if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedPosition)) - { - PromotePrompt.Show(SessionName, new MovePieceCommand - { - From = selectedPosition, - To = position - }); - } - else - { - await ShogiApi.Move(SessionName, new MovePieceCommand - { - From = selectedPosition, - IsPromotion = false, - To = position - }); - } - } - } - - void OnClickHand(Piece piece) - { - selectedPiece = piece.WhichPiece; - } -} diff --git a/Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor b/Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor new file mode 100644 index 0000000..b18b89f --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor @@ -0,0 +1,11 @@ +@using Contracts.Types; + + + +@code { + protected override void OnInitialized() + { + base.OnInitialized(); + Console.WriteLine("Empty Game Board."); + } +} diff --git a/Shogi.UI/Pages/Home/GameBoard/GameBoard.razor b/Shogi.UI/Pages/Home/GameBoard/GameBoard.razor new file mode 100644 index 0000000..6345ec6 --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBoard/GameBoard.razor @@ -0,0 +1,45 @@ +@using Shogi.Contracts.Api +@using Shogi.Contracts.Types; +@using System.Text.RegularExpressions; +@inject IShogiApi ShogiApi +@inject AccountState Account; +@inject PromotePrompt PromotePrompt; + +@if (session == null) +{ + +} +else if (isSpectating) +{ + +} +else +{ + +} + + +@code { + [Parameter] + public string? SessionName { get; set; } + + Session? session; + private WhichPlayer perspective; + private bool isSpectating; + + protected override async Task OnParametersSetAsync() + { + if (!string.IsNullOrWhiteSpace(SessionName)) + { + this.session = await ShogiApi.GetSession(SessionName); + if (this.session != null) + { + var accountId = Account.User?.Id; + + this.perspective = accountId == session.Player2 ? WhichPlayer.Player2 : WhichPlayer.Player1; + this.isSpectating = !(accountId == this.session.Player1 || accountId == this.session.Player2); + Console.WriteLine($"IsSpectating - {isSpectating}. AccountId - {accountId}. Player1 - {this.session.Player1}. Player2 - {this.session.Player2}"); + } + } + } +} diff --git a/Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor b/Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor new file mode 100644 index 0000000..3fe2bcb --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor @@ -0,0 +1,116 @@ +@using Shogi.Contracts.Types; +@inject PromotePrompt PromotePrompt; + +
+ +
+ @for (var rank = 1; rank < 10; rank++) + { + foreach (var file in Files) + { + var position = $"{file}{rank}"; + var piece = Session?.BoardState.Board[position]; + var isSelected = piece != null && SelectedPosition == position; +
+ +
+ } + } +
+ 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 +
+
+ A + B + C + D + E + F + G + H + I +
+ +
+

Do you wish to promote?

+
+ + + +
+
+
+ + @if (Session != null) + { + + } +
+ +@code { + [Parameter] public WhichPlayer Perspective { get; set; } + [Parameter] public Session? Session { get; set; } + [Parameter] public string? SelectedPosition { get; set; } + // TODO: Exchange these OnClick actions for events like "SelectionChangedEvent" and "MoveFromBoardEvent" and "MoveFromHandEvent". + [Parameter] public Action? OnClickTile { get; set; } + [Parameter] public Action? OnClickHand { get; set; } + + static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" }; + + private IReadOnlyCollection OpponentHand + { + get + { + if (this.Session == null) return Array.Empty(); + + return Perspective == WhichPlayer.Player1 + ? this.Session.BoardState.Player1Hand + : this.Session.BoardState.Player2Hand; + } + } + IReadOnlyCollection UserHand + { + get + { + if (this.Session == null) return Array.Empty(); + + return Perspective == WhichPlayer.Player1 + ? this.Session.BoardState.Player1Hand + : this.Session.BoardState.Player2Hand; + } + } + + private Action OnClickTileInternal(Piece? piece, string position) => () => OnClickTile?.Invoke(piece, position); + private Action OnClickHandInternal(Piece piece) => () => OnClickHand?.Invoke(piece); +} diff --git a/Shogi.UI/Pages/Home/GameBoard.razor.css b/Shogi.UI/Pages/Home/GameBoard/GameboardPresentation.razor.css similarity index 100% rename from Shogi.UI/Pages/Home/GameBoard.razor.css rename to Shogi.UI/Pages/Home/GameBoard/GameboardPresentation.razor.css diff --git a/Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor b/Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor new file mode 100644 index 0000000..b10d553 --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor @@ -0,0 +1,71 @@ +@using Shogi.Contracts.Api; +@using Shogi.Contracts.Types; +@using System.Text.RegularExpressions; +@inject PromotePrompt PromotePrompt; +@inject IShogiApi ShogiApi; + + + +@code { + [Parameter] public WhichPlayer Perspective { get; set; } + [Parameter] public Session Session { get; set; } + private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective; + private string? selectedBoardPosition; + private WhichPiece? selectedPieceFromHand; + + bool ShouldPromptForPromotion(string position) + { + if (Perspective == WhichPlayer.Player1 && Regex.IsMatch(position, ".[7-9]")) + { + return true; + } + if (Perspective == WhichPlayer.Player2 && Regex.IsMatch(position, ".[1-3]")) + { + return true; + } + return false; + } + + async void OnClickTile(Piece? piece, string position) + { + if (!IsMyTurn) return; + + if (selectedBoardPosition == null || piece?.Owner == Perspective) + { + // Select a position. + selectedBoardPosition = position; + return; + } + if (selectedBoardPosition == position) + { + // Deselect the selected position. + selectedBoardPosition = null; + return; + } + if (piece == null) + { + if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedBoardPosition)) + { + PromotePrompt.Show(Session.SessionName, new MovePieceCommand + { + From = selectedBoardPosition, + To = position + }); + } + else + { + await ShogiApi.Move(Session.SessionName, new MovePieceCommand + { + From = selectedBoardPosition, + IsPromotion = false, + To = position + }); + } + } + } + + void OnClickHand(Piece piece) + { + selectedPieceFromHand = piece.WhichPiece; + } +} diff --git a/Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor b/Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor new file mode 100644 index 0000000..946c385 --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor @@ -0,0 +1,8 @@ +@using Contracts.Types; + +

You are spectating

+ + +@code { + [Parameter] public Session Session { get; set; } +} diff --git a/Shogi.UI/Pages/Home/GameBrowser.razor b/Shogi.UI/Pages/Home/GameBrowser.razor index a646d1a..a00a961 100644 --- a/Shogi.UI/Pages/Home/GameBrowser.razor +++ b/Shogi.UI/Pages/Home/GameBrowser.razor @@ -1,6 +1,7 @@ -@using Shogi.Contracts.Types -@using System.ComponentModel.DataAnnotations -@using System.Net +@using Shogi.Contracts.Types; +@using System.ComponentModel.DataAnnotations; +@using System.Net; +@using System.Text.Json; @inject IShogiApi ShogiApi; @inject ShogiSocket ShogiSocket; @inject AccountState Account; @@ -17,12 +18,27 @@
+

Games you're seated at

- @if (!sessions.Any()) + @if (!joinedSessions.Any()) { -

No games exist

+

You have not joined any games.

} - @foreach (var session in sessions) + @foreach (var session in joinedSessions) + { + + } +
+

Other games

+
+ @if (!otherSessions.Any()) + { +

You have not joined any games.

+ } + @foreach (var session in otherSessions) {
} diff --git a/Shogi.UI/Program.cs b/Shogi.UI/Program.cs index a832802..f79ea61 100644 --- a/Shogi.UI/Program.cs +++ b/Shogi.UI/Program.cs @@ -28,14 +28,15 @@ static void ConfigureDependencies(IServiceCollection services, IConfiguration co services .AddHttpClient(ShogiApi.GuestClientName, client => client.BaseAddress = shogiApiUrl) .AddHttpMessageHandler(); - services - .AddHttpClient(ShogiApi.AnonymouseClientName, client => client.BaseAddress = shogiApiUrl) - .AddHttpMessageHandler(); + //services + // .AddHttpClient(ShogiApi.AnonymousClientName, client => client.BaseAddress = shogiApiUrl) + // .AddHttpMessageHandler(); // Authorization services.AddMsalAuthentication(options => { configuration.Bind("AzureAd", options.ProviderOptions.Authentication); + options.ProviderOptions.LoginMode = "redirect"; }); services.AddOidcAuthentication(options => { diff --git a/Shogi.UI/Shared/ShogiSocket.cs b/Shogi.UI/Shared/ShogiSocket.cs index 7f64877..3dbb5d0 100644 --- a/Shogi.UI/Shared/ShogiSocket.cs +++ b/Shogi.UI/Shared/ShogiSocket.cs @@ -54,6 +54,7 @@ public class ShogiSocket : IDisposable switch (action) { case SocketAction.SessionCreated: + Console.WriteLine("Session created event."); this.OnCreateGameMessage?.Invoke(this, JsonSerializer.Deserialize(memory, this.serializerOptions)!); break; default: diff --git a/Shogi.UI/Shogi.UI.csproj b/Shogi.UI/Shogi.UI.csproj index a62392d..2d6179c 100644 --- a/Shogi.UI/Shogi.UI.csproj +++ b/Shogi.UI/Shogi.UI.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 enable enable @@ -14,12 +14,12 @@ - - - + + + - - + + diff --git a/Shogi.UI/_Imports.razor b/Shogi.UI/_Imports.razor index 7d78d6c..0cdacaf 100644 --- a/Shogi.UI/_Imports.razor +++ b/Shogi.UI/_Imports.razor @@ -10,6 +10,7 @@ @using Microsoft.JSInterop @using Shogi.UI.Pages.Home.Account @using Shogi.UI.Pages.Home.Api +@using Shogi.UI.Pages.Home.GameBoard @using Shogi.UI.Pages.Home.Pieces @using Shogi.UI.Shared.Modal @using Shogi.UI.Shared diff --git a/Tests/AcceptanceTests/AcceptanceTests.csproj b/Tests/AcceptanceTests/AcceptanceTests.csproj index b9b2f07..75bd680 100644 --- a/Tests/AcceptanceTests/AcceptanceTests.csproj +++ b/Tests/AcceptanceTests/AcceptanceTests.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 enable enable @@ -23,11 +23,11 @@ - + - + diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index 180ddf4..cf04e8c 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 enable false diff --git a/global.json b/global.json index 68eeba6..b48bb00 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.300", + "version": "7.0.2", "rollForward": "latestFeature" } } \ No newline at end of file